Skip to content

feat: schema-typed File<T> accessors for schema-declared sources - #23

Open
neilverc wants to merge 1 commit into
mainfrom
typed-hooks
Open

feat: schema-typed File<T> accessors for schema-declared sources#23
neilverc wants to merge 1 commit into
mainfrom
typed-hooks

Conversation

@neilverc

@neilverc neilverc commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What

A source can declare a JSON Schema (schema), same as before. What's new: every declared source's generated FS accessor is now typed to that schema. fs.getSourcesDeploymentYaml() still exists exactly as it always did, but File is generic (File<T = string>) — for a schema-declared source, T is the schema's generated interface instead of the plain-string default, so getContent() returns the parsed, already-valid object and setContent() takes one back, validating + re-serializing on the way out. No more hand-rolled load(file.getContent()) as Deployment casts trusted on faith.

This lives entirely in the FS/File mechanism, not as a separate hook type or a per-entry binding: RenderHook, post_render, dependents, and ValidateHook all still take (ctx, fs) exactly as before, and a schema-declared source's typed accessor works identically wherever that FS shows up — kind hooks, resource hooks, or a dependent hook reaching into the consumer's FS.

Wire format

  • sources entries are polymorphic: a bare string, or {path, schema?}. Every existing kind.json with a plain string array keeps working unchanged.
  • Compiled Kind inlines each schema-declared source's raw JSON Schema text under a new source_schemas map (same key convention as sources) — self-contained, no filesystem access needed at render time.
  • RenderHookDefinition (hook entries under render/post_render/validate) is unchanged{path, access?}. There's no per-entry source binding; typing is a property of the source, surfaced through the FS accessor, not something a hook opts into.

Schema enforcement — two checks

A source's schema is a contract on the source, enforced identically regardless of which accessor a hook uses to reach it (generated typed accessor or the fs.get() escape hatch):

  1. Pre-render gate — before any hook runs, every schema-declared source's initial content is parsed + validated, all in one pass. Violations are collected and reported together (same aggregated channel as hooks.validate), not fail-fast.
  2. Validated on every access — every getContent()/setContent() call against a schema-declared source validates against the schema at that exact call, synchronously. A bad write throws immediately, aborting the render and attributing the failure to whichever hook made it. Because every access point is itself a checkpoint, there's no separate bulk re-check needed after the fact — nothing like a "final check" stage exists.

Type generation

veil build emits one TS interface per unique schema file a kind's sources reference (named after the file, e.g. kubernetes-deployment.schema.jsonKubernetesDeployment) into the kind's veil-types.ts, and makes File generic there too. fsInterfaceNamed resolves each schema-declared source's accessor to File<T> for its schema's interface; a source with no schema keeps the plain File (File<string>) accessor.

Scope

Applies uniformly wherever an FS is handed to a hook: kind-level render/post_render/validate, resource-level metadata.hooks.render, and hooks.dependents (a dependent hook reaching into the consumer's FS gets the consumer's typed accessors too, for free — there's no special-casing needed since typing rides with the FS/File mechanism, not the hook's own registration).

Testing

  • pkg/hook: unit tests against the raw WithTypedSources/WithSchemaValidate options — typed-accessor round trip (JSON + YAML codec), rejecting an invalid write, and proving the fs.get() escape hatch is validated too (but always stays string-typed).
  • pkg/render: end-to-end — typed-accessor round trip, invalid-write rejection (attributed to the offending hook), pre-render gate rejecting bad initial content, escape-hatch corruption caught immediately (no deferred "final check"), typed accessor + untyped hook interleaving, and a schema-less source's accessor staying a plain unvalidated string.
  • pkg/commands: compiled kind.json carries source_schemas; generated veil-types.ts has the schema interface plus the generic File<T = string> accessor signature.
  • pkg/config: source schema-file-existence validation at load time (hook-source binding validation removed along with the binding itself).
  • SPEC.md: sources/hooks field docs, File/FS interface, new "Schema-typed sources" section (replaces "Typed hooks"), "Schema enforcement" (two checks, not three), dependent-hooks scope note.

Manually verified end-to-end against a real project (build → typecheck → render, happy path and a deliberate schema violation) outside this repo.

@neilverc neilverc changed the title feat: typed render hooks bound to schema-declared sources feat: schema-typed File<T> accessors for schema-declared sources Aug 27, 2026
A source can now declare a `schema` (unchanged from before), and every
declared source gets a generated `FS` accessor as always -
`fs.getSourcesDeploymentYaml()`. What's new: `File` is generic
(`File<T = string>`), and a schema-declared source's accessor returns
`File<T>` for that schema's generated interface instead of the plain-string
default. `getContent()`/`setContent()` on it deal in the parsed,
schema-valid object directly - no manual `JSON.parse`/`load`/`dump`, no
`as` cast trusted on faith.

This lives entirely in the FS/File mechanism, not in a separate hook
type: `RenderHook`, `post_render`, `dependents`, and `ValidateHook` all
still take `(ctx, fs)`, and a schema-declared source's typed accessor
works identically wherever that FS shows up - kind hooks, resource
hooks, or a dependent hook reaching into the consumer's FS.

Enforcement is two synchronous checks, not three:

1. Pre-render gate - every schema-declared source's initial content is
   parsed and validated in one pass before any hook runs.
2. Validated on every access - every getContent()/setContent() call
   against a schema-declared source, through the typed accessor or the
   `fs.get()` escape hatch, validates against the schema at that exact
   call. A bad write throws immediately, attributed to the hook that
   made it. Because every access point is itself a checkpoint, there's
   no bulk re-check needed after the fact.

Wire format: `sources` entries are polymorphic (bare string, or
`{path, schema?}`); compiled `Kind` carries each schema-declared
source's raw schema text under `source_schemas`. Hook entries
(`RenderHookDefinition`) are unchanged - `{path, access?}`, no
per-entry source binding.

pkg/hook gains `WithTypedSources`/`WithSchemaValidate` options, kept
decoupled from any concrete JSON Schema library - the validator is a
caller-supplied closure. pkg/build's generated `veil-types.ts` makes
`File` generic and threads each schema's interface through
`fsInterfaceNamed`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant