@nighthq/manifest
v0.2.0
Published
The substrate under a plugin seam: opaque versioned ids, declared parameter schemas, and one walker that gates values in both directions.
Readme
@nighthq/manifest
The substrate under a plugin seam: an opaque versioned id, a declared schema, and one walker that holds a value to it.
npm install @nighthq/manifestimport { parseManifestId, validateParams, type ParamSpec } from '@nighthq/manifest';
const params: Record<string, ParamSpec> = {
hue: { kind: 'enum', values: ['shorter', 'longer'], optional: true },
steps: { kind: 'number', min: 1, integer: true }
};
parseManifestId('ng.oklch/1'); // { name: 'ng.oklch', version: 1 }
parseManifestId('ng.oklch'); // throws — a manifest id carries its version
validateParams(params, { steps: 3, hue: 'shorter' }); // []
validateParams(params, { steps: 0, wobble: true });
// [ 'param "steps" is 0, below its minimum 1',
// 'param "wobble" is not declared by the schema' ]That is the whole package: a name nobody parses meaning out of, a schema that says what may be supplied with it, and errors in both directions — a value that does not fit the schema, and a value the schema never declared.
Why it is its own package
Every plugin seam re-invents these three pieces, and re-inventing them is how
two systems end up disagreeing about what "1" means. This is the version that
@nighthq/icons runs on both sides of its own seam: the tier registry declares
each tier's fields in this vocabulary, the operation registries (transition
samplers, exporters) declare their parameters in it, and one walker gates every
value at every application site.
It knows nothing about icons, and it imports nothing — not a framework, not the
DOM, not a runtime dependency. Its own lib is ["ES2022"].
The vocabulary
| kind | declares | notable |
| --------- | ----------------------- | ------------------------------------------------------ |
| number | min, max, integer | bounds are inclusive |
| string | nonEmpty, pattern | pattern is a RegExp |
| boolean | — | |
| enum | values | an empty values is a schema error, not a value error |
| array | of, minItems | recursive |
| record | of | open keys, one value shape |
| object | fields | CLOSED — an undeclared key is an error |
| tuple | items | fixed length, per-position shapes |
| unknown | nothing | see below |
Every kind takes optional and a describe string.
unknown is a confession, not an escape hatch
A schema owns the SHAPE of a value and never its LAWS. Some fields are all
law — a reference that must resolve, a closure that must hold, a literal that
must match a token's default — and no shape check says anything true about
them. Declaring such a field unknown records exactly that: this field is
present, and its rules live in code elsewhere.
The alternative is worse than useless: a plausible-looking shape declaration that passes while the actual rule goes unchecked reads, to every reader and every gate, as though the field were verified.
Both directions, always
validateParamSpecs checks the SCHEMA (an enum with no values, an array with
no element shape); validateValue and validateParams check a VALUE against
it. The pair matters: checking only that supplied values fit misses the failure
that actually happens — a manifest grows a parameter and a caller keeps passing
the old set, or a caller passes a key the manifest never declared and it is
silently ignored.
Both return string[] — empty is a pass. Nothing here throws except
parseManifestId, which has nothing to return when an id is malformed.
Ids are opaque and versioned
name/version: lowercase dotted name, positive integer version, ng.lerp/1.
The consumer of an id never parses meaning out of it — a registry is the only
resolver, which is what lets a third party register acme.thing/2 without the
host package changing, and what makes an unregistered id a refusal rather than
a fallback.
