feat: importable validation harness (lib/validate.mjs) - #34
Conversation
The harness's checks were already pure functions trapped in a CLI script;
this makes the one-validator principle (rfc/dx3-bootstrap-design §4)
importable instead of aspirational:
- lib/validate.mjs: every check moved verbatim — buildVocabulary,
checkVocabulary (S2), checkGovernance (incl. S1 over examples),
checkCategories, validateDocument — plus compileSchemaSet (schemas
INJECTED by the caller: no filesystem, no process control, ajv-only
imports, so the identical code runs in a browser bundle) and two small
additions: stripAdditiveBlocks (the back-compat strip, previously inline
in main) and documentReport ({valid, version, errors} for hosts).
- scripts/validate.mjs: now the filesystem-and-process front-end over the
lib — a distribution of the harness, never a fork. CLI output is
byte-identical to the pre-extraction harness (diffed; only npm's version
banner changes).
- scripts/check-lib-boundary.mjs (CI-wired as check:lib): purity gate
(ajv-only imports) + non-vacuity through the IMPORT surface — all
examples accepted (with back-compat strip), all 18 negative fixtures
rejected, via the lib directly.
- lib/validate.d.ts hand-written types; lib/ added to files; README
programmatic-validation section; version 0.4.2.
No exports map, deliberately: every published file stays deep-importable
(af-site and siblings resolve schema/ and examples/ by path today).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final release verification (train pass)
0.4.2 release notes:
Publish after merge: |
There was a problem hiding this comment.
🟡 Not ready to approve
The new library can throw at runtime if the surface schema isn’t injected, and the README example uses a JSON import syntax inconsistent with the repo’s declared Node >=20 support.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR extracts the dspack validation harness into an importable, pure ESM library (lib/validate.mjs) so programmatic consumers (including browser bundles) can reuse the exact same validation checks and error strings as the CLI, while keeping the CLI as a filesystem/process front-end.
Changes:
- Added
lib/validate.mjs(+lib/validate.d.mts) exporting schema compilation and document validation utilities. - Refactored
scripts/validate.mjsto delegate all validation logic to the library. - Added a CI gate (
scripts/check-lib-boundary.mjs, wired vianpm run check:liband the workflow) to enforce library purity and corpus validation through the import surface.
File summaries
| File | Description |
|---|---|
| scripts/validate.mjs | Reworked CLI to use lib/validate.mjs for all checks and back-compat stripping. |
| scripts/check-lib-boundary.mjs | New CI gate: verifies library import purity and runs the corpus through the library API. |
| README.md | Documents programmatic validation via the new importable harness. |
| package.json | Bumps version to 0.4.2, publishes lib/, and adds check:lib script. |
| lib/validate.mjs | New pure validation library (Ajv-only imports; injected schemas). |
| lib/validate.d.mts | New TypeScript declarations for the library API. |
| .github/workflows/validate.yml | Adds npm run check:lib to CI. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| if (GOVERNANCE_VERSIONS.has(version)) { | ||
| errors.push(...checkGovernance(doc, validators.get(SURFACE_SCHEMA))); | ||
| } |
| import v04 from "@aestheticfunction/dspack-spec/schema/dspack.v0.4.schema.json" with { type: "json" }; | ||
| import surface from "@aestheticfunction/dspack-spec/schema/dspack.surface.v0_1.schema.json" with { type: "json" }; |
What
Extracts the validation harness into an importable, pure library — the repo's own RFC calls the current state a violation of the one-validator principle ("the published CLI is a distribution of the harness, never a fork", rfc/dx3-bootstrap-design §4). The consuming feature is dspack-studio's Phase 2 composer: in-browser document validation with the same checks and wording as the CLI.
lib/validate.mjs— every check moved verbatim (S2checkVocabulary,checkGovernancewith S1 overexamples[],checkCategories,validateDocument,buildVocabulary);compileSchemaSettakes schemas injected by the caller (no fs — Node reads files, a browser bundler imports the JSON); plusstripAdditiveBlocks(the back-compat strip, previously inline inmain) anddocumentReport({valid, version, errors}for hosts). Imports: ajv + ajv-formats only.scripts/validate.mjs— now the filesystem/process front-end over the lib. All three modes (default,--fixtures negative,--file) unchanged; CLI output byte-identical to the pre-extraction harness (diffed; the only delta is npm's version banner).scripts/check-lib-boundary.mjs(CI-wired ascheck:lib) — purity gate (ajv-only imports) + non-vacuity through the import surface: 2 examples accepted incl. back-compat strip, 18 negative fixtures rejected, via the lib directly. If extraction ever drifts a check, this gate and the CLI disagree loudly.lib/validate.d.tshand-written types;lib/infiles; README "Validating dspack files programmatically" section; version 0.4.2.Deliberate choices
schema/*.jsonandexamples/*.dspack.jsonby path today (af-site's gates, ds-mcp's sync checks); an exports map would break them. The lib is imported by file path:@aestheticfunction/dspack-spec/lib/validate.mjs.validateDocumentreturns the exact strings the CLI prints (paths embedded) — zero semantic drift, wording shared by construction. A structured-findings shape can layer on later without touching the checks.Verification
npm run validate(2 examples PASS),--fixtures negative(18/18 rejected),--fileboth directions — all green, output byte-identical vs old harness.npm run check:libPASS (purity + full corpus through the import).check:libstep between negative fixtures and file mode.Release
Version 0.4.2 in-branch (package-level additive change; the spec itself is untouched, so no 0.5 signal). Publishes via the existing tag-triggered OIDC workflow (
git tag v0.4.2) after merge.🤖 Generated with Claude Code