ADR
Surface license state in component output
Context
@directededges/anova currently exports a Metadata type with six fields (author, lastUpdated, generator, schema, source, config). There is no field to carry license state in the serialised component output.
Downstream tools need a standard place in the component spec to read whether a valid license was present at generation time and what level of output that license grants. This ADR evaluates where that field belongs and how license data flows into the transformer.
Current Metadata shape (abbreviated):
Metadata: author: string lastUpdated: string generator: { url, version, name } schema: { url, version } source: { pageId, nodeId, nodeType } config: Config # license — ABSENTDecision Drivers
- Type-schema symmetry: Constitution I requires every type change have a corresponding schema change before publishing; drift is a bug.
- Additive only: The
licensefield MUST be optional so existing consumers that do not supply a license receive identical output — no MAJOR bump. Whengenerator.licenseis absent, runtimes assume the most basic entitlement (status: "NONE",level: "FREE"). - No logic: Constitution II forbids algorithms or runtime behaviour; this ADR introduces only type declarations and schema properties.
- Stable, intentional API: Constitution III requires that every new export represent a genuine shared concept.
license?onMetadatais a standard output field consumed by all downstream packages. - Strict TypeScript: All new declarations must compile under strict mode with zero errors (Constitution V).
Boundary: Config does not carry license data
Config controls output shape — layout, formatting, depth — and is a caller-supplied input that the transformer consumes deterministically. License state is not a formatting preference; it is an entitlement resolved by the runtime environment (CLI credentials, plugin auth context) and should not leak into Config.
Instead, the runtime (anova-kit CLI, anova-plugin) resolves license state independently and supplies it to the transformer as a separate input alongside Config. The transformer stamps the resolved result into the output’s generator block. This keeps Config purely about what shape the output takes and generator.license about what the caller was entitled to.
There are interdependencies between the two: certain Config options (e.g. advanced layout modes, extended detail depth) may only be available at higher license levels. Enforcement of those constraints is a runtime concern — the transformer or the runtime validates that the requested Config is compatible with the resolved license level before generation, but Config itself remains unaware of licensing.
Options Considered
Option A — Top-level metadata.license?
Add a new optional license property directly on Metadata, as a sibling of generator, schema, etc.
Metadata: ... config: Config license?: status: string description: stringPros: Clear separation — license is its own concern, distinct from generator identity.
Cons: Adds a seventh top-level key to Metadata. License state is inherently tied to what the generator produced, yet lives outside generator.
Option B — Nested inside generator.license?
Extend the existing generator object with an optional license sub-object. The generator already describes what produced the output; license describes what the output is entitled to.
Metadata: ... generator: url: string version: number name: string license?: status: string # e.g. "VALID", "EXPIRED", "NONE" level: string # e.g. "FREE", "PRO", "EXTENDED"Pros:
- Flatter: reuses an existing object rather than adding a new top-level key.
- Semantically grouped:
generatoralready answers “who made this and how” — adding “under what license” is a natural extension. levelcommunicates what richness the output may (or may not) contain, making it immediately useful for downstream gating.levelis a plainstringfor now, keeping it extensible as tiers evolve.
Cons: Slightly overloads the generator concept (identity + entitlement).
Decision
Option B — add license? inside generator.
Type changes (types/)
| File | Change | Bump |
|---|---|---|
Metadata.ts | Add optional license? to the generator object | MINOR |
Updated Metadata shape (types/Metadata.ts):
# Beforegenerator: { url, version, name }
# Aftergenerator: url: string version: number name: string license?: # optional — absent when no license was resolved status: string # e.g. "VALID", "EXPIRED", "NONE" level: string # e.g. "FREE", "PRO", "EXTENDED"Schema changes (schema/)
| File | Change | Bump |
|---|---|---|
component.schema.json | Add license property to #/definitions/Metadata/properties/generator/properties (NOT in generator’s required[]) | MINOR |
Updated generator properties (#/definitions/Metadata/properties/generator):
# New property added to generator/propertieslicense: type: object description: "Resolved license state at the time this component spec was generated. Absent when no license was supplied." properties: status: type: string description: "License validation status (e.g. VALID, EXPIRED, NONE)." level: type: string description: "Output entitlement level (e.g. FREE, PRO, EXTENDED). Plain string for extensibility." required: - status - level additionalProperties: false # license is NOT added to generator's required[] — it remains optionalNotes
licenseis intentionally absent from the generatorrequired[]array. Consumers that do not provide a license receive an identicalgeneratorobject to the current version.additionalProperties: falseongeneratoris retained; addinglicensetopropertiesis the correct mechanism — no schema constraint is relaxed.levelis typed asstringrather than a union/enum to allow the tier model to evolve without a type-level breaking change.
Type ↔ Schema Impact
- Symmetric: Yes
- Parity check:
types/Metadata.ts → generator.license?maps toschema/component.schema.json → #/definitions/Metadata/properties/generator/properties/license- Both changes are optional/non-required in their respective artifacts
Consequences
generatoroutput may now includelicense.statusandlicense.levelwhen a generator resolves a license.- Downstream tools can read
generator.license.levelto decide whether to gate or enrich output (e.g. omit certain sections forFREE). - Any tool performing runtime validation against
component.schema.jsonwill accept the new optional field automatically; no validator changes are required for consumers that never producelicense.