Skip to content

feat: browser-safe validation (schema mirrors, no runtime fs) - #25

Merged
ryandmonk merged 2 commits into
mainfrom
feat/browser-safe-validation
Aug 3, 2026
Merged

feat: browser-safe validation (schema mirrors, no runtime fs)#25
ryandmonk merged 2 commits into
mainfrom
feat/browser-safe-validation

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

What / why

Phase 0 item 5 of the dspack-studio composer plan: the studio's mapper needs live in-browser fidelity feedback, which means running transformFromJson, validateCatalog, and loadProfile in a browser bundle. Both src/validate/ajv.ts and src/transform/profile-load.ts read schema JSON at runtime via fileURLToPath + readFileSync at module scope, which makes the library surface unbundleable for the browser.

Approach: mirror + drift test (the JSONs stay)

The .json files remain the reviewable schema documents — docs/PROFILES.md links src/transform/profile.v1.schema.json, and the README points reviewers at src/validate/meta/. What changes is how the runtime reaches them:

  • src/validate/meta/catalog-meta.ts — committed TS mirror exporting both catalog-shape meta-schemas (catalogMetaSchemas, keyed by A2uiVersion); ajv.ts gate 2 imports it instead of reading files.
  • src/transform/profile-schema.ts — committed TS mirror of profile.v1.schema.json; profile-load.ts imports and re-exports it, so the public profileSchema export is unchanged.
  • src/schema-mirrors.test.ts — the drift gate: reads each .json with node:fs (fs is fine in tests) and asserts toStrictEqual with the TS mirror. A failure means one side was edited without the other.
  • Build script trimmed: both cp steps removed; dist no longer contains or reads JSON at runtime.

Fail-first: boundary test output on unchanged code

src/browser-boundary.test.ts (modeled on dspack-gen's core-boundary test) statically scans every non-test module under src/transform/, src/validate/, and src/targets/ and forbids node:* imports plus bare-specifier built-ins. Run against unchanged main:

 FAIL  src/browser-boundary.test.ts > browser boundary > src/transform/profile-load.ts imports no Node built-ins
AssertionError: src/transform/profile-load.ts imports node:fs: expected true to be false // Object.is equality
 FAIL  src/browser-boundary.test.ts > browser boundary > src/validate/ajv.ts imports no Node built-ins
AssertionError: src/validate/ajv.ts imports node:fs: expected true to be false // Object.is equality
 Test Files  1 failed (1)
      Tests  2 failed | 15 passed (17)

Exactly the two known offenders; no other module in scope imports Node built-ins, so the boundary covers all three target dirs at full width (no carve-outs needed).

Verification

  • npx vitest run: 94 passed (94) — 74 existing + 17 boundary + 3 drift
  • npm run test:pack: pack-and-install, profile, and bin smokes all OK from the packed tarball with the trimmed build
  • npm run build: clean; dist contains zero .json files and no node: imports outside cli.js
  • npm run check:sync: in sync
  • Browser-bundle smoke: esbuild dist/index.js --bundle --platform=browser succeeds (would fail on any unresolved node: import)

Scope boundaries

  • No behavior change: catalogs and reports byte-identical (existing transform tests + goldens enforce this); profileSchema public export unchanged.
  • CLI untouched: src/cli.ts is the Node CLI and stays out of the boundary — it owns filesystem I/O by design.
  • Tests keep using node:fs freely (they run under vitest).

Suggested version: 0.4.1.

🤖 Generated with Claude Code

ryandmonk and others added 2 commits August 3, 2026 17:30
The dspack-studio composer runs transformFromJson, validateCatalog, and
loadProfile in the browser for live fidelity feedback; readFileSync at
module scope made the library surface unbundleable. The reviewable .json
schema documents stay; committed TS mirrors (catalog-meta.ts,
profile-schema.ts) are what the runtime imports, with
schema-mirrors.test.ts as the drift gate and browser-boundary.test.ts
banning Node built-ins across src/transform, src/validate, src/targets.
Build no longer copies JSON into dist. CLI untouched; catalogs and
reports byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ryandmonk

Copy link
Copy Markdown
Contributor Author

Final release verification (train pass)

  • 94/94 tests; boundary gate proves no node:* (or bare built-in) imports across src/transform/, src/validate/, src/targets/ (cli.ts excluded by design); drift tests pin the TS mirrors to the canonical JSONs.
  • Packed-tarball: all three smokes (exports + A-gates, profile byte-identity + fail-closed, installed bin with --profile refusing invalid at exit 2).
  • esbuild dist/index.js --bundle --platform=browser succeeds — direct proof the studio bundler accepts the surface.

0.4.1 release notes:

0.4.1 — browser-safe validation. The catalog gates and loadProfile no longer read files at runtime: the meta-schemas and profile schema are committed TS mirrors, drift-gated against the reviewable JSON documents (which stay in the package). transformFromJson, validateCatalog, emitSurface, and loadProfile now run in browser bundles. No behavior changes; dist ships no JSON.

Publish after merge (manual): npm ci && npm test && npm run test:pack && npm publish from a clean checkout of main.

@ryandmonk
ryandmonk marked this pull request as ready for review August 3, 2026 22:47
Copilot AI review requested due to automatic review settings August 3, 2026 22:47
@ryandmonk
ryandmonk merged commit 10d330d into main Aug 3, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes the library’s validation/profile-loading surface browser-bundleable by removing runtime filesystem reads of JSON schema documents and replacing them with committed TypeScript “schema mirror” modules, backed by tests that prevent mirror/JSON drift and enforce a no-Node-builtins boundary for browser-targeted source directories.

Changes:

  • Replace runtime node:fs schema reads with TS mirror modules for catalog meta-schemas and the profile schema.
  • Add drift tests to assert .json schema documents remain exactly equal to their TS mirrors.
  • Add a “browser boundary” test to forbid Node built-in imports in src/transform, src/validate, and src/targets; simplify the build step accordingly.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/validate/meta/catalog-meta.ts Adds TS mirrors for versioned catalog meta-schemas used at runtime.
src/validate/ajv.ts Switches gate-2 meta-schema loading from runtime FS reads to TS mirror import.
src/transform/profile-schema.ts Adds TS mirror of the profile JSON schema for browser-safe runtime use.
src/transform/profile-load.ts Removes runtime FS schema load; imports/re-exports the TS mirror to keep public export stable.
src/schema-mirrors.test.ts Adds drift tests ensuring JSON schema files and TS mirrors stay identical.
src/browser-boundary.test.ts Adds a boundary test intended to ensure browser-bundle safety of library modules.
scripts/pack-test.sh Updates commentary to reflect “schemas compiled in” packaging expectations.
package.json Simplifies build (removes JSON copy steps) and bumps version to 0.4.1.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +38 to +41
const source = readFileSync(file, "utf8");
const imports = [...source.matchAll(/from\s+"([^"]+)"|import\s*\(\s*"([^"]+)"\s*\)|require\s*\(\s*"([^"]+)"\s*\)/g)].map(
(m) => m[1] ?? m[2] ?? m[3],
);
Comment on lines +13 to +23
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

const BOUNDARY_DIRS = ["src/transform", "src/validate", "src/targets"];

// Bare-specifier built-ins that would slip past a `node:` prefix check.
const BARE_BUILTINS = new Set([
"fs", "path", "url", "os", "crypto", "util", "stream", "buffer",
"http", "https", "net", "tls", "child_process", "worker_threads",
]);
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.

2 participants