So, discovery is working. Now we need to define what we are discovering.
This is where the schemas come in.
The goal is to have a strict contract for every part of the system. If Velnora detects a package, it needs to know exactly what that package is capable of.
The first question was: what does a discovered project actually look like at runtime? I needed a Project interface — a strict contract that captures everything the system needs to know about a package.
But package.json alone isn't enough. It gives you the name, version, and dependencies — but nothing Velnora-specific. Where does the adapter config go? The framework hints? The build targets? Polluting package.json with custom fields felt wrong.
So the Project interface pulls from two sources of truth:
packageJson — the raw package.json, stored as-is so adapters and plugins can inspect dependencies without re-reading the file.config — the resolved VelnoraAppConfig from velnora.config.ts (or an empty object if none exists). This is where all Velnora-specific metadata lives.On top of that, each project gets:
name derived from the workspace root (e.g., packages/app-one).displayName from package.json (e.g., @example/app-one).root path and a routable path for the Host.The configuration itself works at two distinct levels:
VelnoraConfig (Global): Lives in the workspace root. Defines the rules for the entire repo — plugins, shared defaults, and global overrides.VelnoraAppConfig (App-Specific): Lives inside each project folder. Defines the specific framework, build targets, and unique needs of that application. This is what ends up in Project.config.By separating them, we get strict typing for each context while keeping the overall system cohesive.
But as satisfying as it was to define these interfaces, I realized something.