Skip to content

feat: profile-as-data — loadProfile, profile.v1 schema, scaffoldProfile, CLI --profile + bin - #24

Merged
ryandmonk merged 2 commits into
mainfrom
feat/profile-as-data
Aug 3, 2026
Merged

feat: profile-as-data — loadProfile, profile.v1 schema, scaffoldProfile, CLI --profile + bin#24
ryandmonk merged 2 commits into
mainfrom
feat/profile-as-data

Conversation

@ryandmonk

@ryandmonk ryandmonk commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

Makes the a2ui mapping Profile reachable as data — the seam a visual catalog composer edits. First PR of a three-repo release train (this → dspack-gen#48 → dspack-studio#27).

  • src/transform/profile.v1.schema.json — draft-2020-12 schema mirroring the Profile interface 1:1, plus a profileVersion: "1" envelope const. Shipped in the package (dist/transform/). One deliberate, documented extension beyond the TS type: x-* keys are accepted and preserved (document/plan/casualty levels) — the dspack convention, so authoring tools can carry provenance without schema changes; the engine never reads them. Everything else fails closed (additionalProperties: false, tested at document and plan level).
  • loadProfile(json) — ajv 2020 / strict:false / ajv-formats (the exact toolchain of the catalog gates); throws typed ProfileLoadError with pathed issues; strips the envelope. A JSON profile drives the engine byte-identically to the TS profile (tested in-repo AND from the packed tarball).
  • scaffoldProfile(doc, { catalogIdBase }) — mechanical 1:1 draft seeded from the json-render CatalogModel: verbatim prop projections, subFlatten derived from declared acceptsChildren, no valueMaps, no casualties. Every judgment call returned as notes (the profile analog of dspack-export's awaitingAuthorship). No governance invented.
  • CLI: --profile <path> (schema refusal → exit 2), package bin dspack-emit (shebang now plain node), repo-relative sample-surface default skipped under --profile so out-of-repo contracts emit from any CI.
  • Casualty refusal cites the authored reason — a surface using a declared casualtyComponents id fails with the profile's class + reason instead of a bare unknown-component error.

Verification

  • 72/72 tests (10 new): round-trip byte-identity vs shadcnProfile; schema negative controls (kind:"vibes", non-https catalogIdBase, missing envelope, unknown keys at document + plan level with the x-* positive control); scaffold validity + full coverage + verbatim-enum seeding on both canonical contracts; casualty citation (landed fail-first — output below).
  • npm run test:pack proves from the installed tarball: exports map intact, A1–A3 pass, typed errors, JSON-profile byte-identity, loadProfile fail-closed, and the installed dspack-emit bin emitting with --profile + refusing an invalid profile at exit 2.
  • External smoke (composer spike): a non-canonical 5-component contract emitted via a hand-written JSON profile from an unrelated directory — A1/A2/A3 PASS both A2UI versions, 6 real instances validated; also exercised kind:"string"/"boolean" propMaps (previously untested by in-repo profiles).

Fail-first evidence (casualty citation, run against pre-change surface.ts):

AssertionError: expected 'unknown component \'dropdown-menu\': …' to contain 'declared casualty'

Scope boundaries

No engine/mapping behavior changes; no changes to existing emitted artifacts (goldens untouched, out/ regenerates identically for the shadcn profile); json-render target untouched; no new runtime dependencies.

Risks & rollback

  • Additive API only; existing consumers (dspack-gen, dspack-studio contracts) are unaffected at ^0.3.x behavior. The one behavior-adjacent change is the casualty error message text — nothing parses that string (verified in dspack-gen and dspack-studio; the studio slice asserts on the NEW text).
  • Rollback: revert the merge; no migrations, no data formats change (profile.v1 documents simply stop loading, TS profiles unaffected).
  • The bin shebang change (npx tsxnode) only affects the new bin path; in-repo npm run transform still uses tsx.

Release

Version: 0.4.0 (bumped in this PR). Publish is manual and stays with you:

git checkout main && git pull && npm ci && npm test && npm run test:pack && npm publish

Suggested release notes:

0.4.0 — profiles as data. loadProfile + shipped profile.v1.schema.json (1:1 mirror of the Profile type; x-* extension slots; fail-closed with pathed errors), scaffoldProfile mechanical drafts, dspack-emit bin with --profile, and casualty refusals that cite the authored reason. Additive; no engine behavior changes.

Downstream: dspack-gen#48 widens its emit range to admit 0.4.x; dspack-studio#27 replaces its temporary override with ^0.4.0 once this is on npm.

🤖 Generated with Claude Code

…le, CLI --profile + bin

The Profile has always been pure data (profiles.ts); this makes it reachable
as data, which is the seam a visual catalog composer edits:

- src/transform/profile.v1.schema.json: draft-2020-12 schema mirroring the
  Profile interface 1:1 plus a profileVersion:"1" envelope const.
- loadProfile(json): ajv-validated (same toolchain as the catalog gates),
  typed ProfileLoadError with pathed issues; strips the envelope. A JSON
  profile drives the engine byte-identically to the TS profile (tested).
- scaffoldProfile(doc, {catalogIdBase}): mechanical 1:1 draft seeded from the
  json-render CatalogModel — verbatim prop projections, subFlatten derived
  from declared acceptsChildren, no valueMaps, no casualties; every judgment
  call returned as notes (the awaitingAuthorship analog for profiles).
- CLI: --profile <path> (exit 2 on schema refusal), bin "dspack-emit"
  (shebang now plain node), sample-surface default skipped under --profile
  so external contracts emit from any CI.
- Casualty refusal now cites the authored reason: a surface using a declared
  casualtyComponents id fails with the profile's class + reason instead of a
  bare unknown-component error (fail-first output in the PR).

All 71 tests pass incl. 9 new (round-trip byte-identity, schema negative
controls, scaffold coverage on both canonical contracts, casualty citation);
test:pack OK with the new bin + shipped schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n + profile smoke

- version 0.4.0 (additive API: loadProfile/profileSchema/ProfileLoadError/
  scaffoldProfile, CLI --profile + bin, casualty-reason refusals).
- New test: unknown keys refuse at document and plan level; x-* is the one
  sanctioned extension surface (dspack convention), preserved by loadProfile
  and documented in PROFILES.md — explicit, not silent.
- pack-test now also proves, from the installed tarball: JSON-profile
  byte-identity with the TS profile, loadProfile fail-closed, and the
  installed dspack-emit bin emitting with --profile and refusing an invalid
  profile at exit 2.

72/72 tests; pack-and-install + profile + bin smokes OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ryandmonk
ryandmonk marked this pull request as ready for review August 3, 2026 19:20
Copilot AI review requested due to automatic review settings August 3, 2026 19:20

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 A2UI mapping Profile consumable “as data” by introducing a versioned JSON profile document format (schema + loader), adding a mechanical scaffold generator from a dspack contract, and wiring both into the CLI (including publishing a dspack-emit bin). It also improves surface emission errors by citing authored casualty reasons.

Changes:

  • Add v1 JSON schema for Profile plus loadProfile()/ProfileLoadError for fail-closed runtime validation.
  • Add scaffoldProfile() to generate a valid v1 profile document + judgment notes from a contract’s json-render model.
  • Extend the CLI with --profile <path> and package a dspack-emit bin; improve casualty refusal messages in the surface emitter; add tests and docs.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/transform/scaffold.ts Adds scaffoldProfile() to mechanically derive a v1 profile document + notes from a contract.
src/transform/profile.v1.schema.json Introduces draft-2020-12 schema for profile v1 with fail-closed validation and x-* extension slots.
src/transform/profile-load.ts Adds loadProfile() + ProfileLoadError using Ajv 2020-12 and exports the shipped schema.
src/targets/a2ui/surface.ts Enhances missing-component errors to cite declared casualty class + reason when applicable.
src/profile-load.test.ts Adds tests for schema validation, byte-identity behavior, scaffold validity, and casualty refusal messaging.
src/index.ts Exposes loadProfile, profileSchema, and scaffoldProfile from the package entrypoint.
src/cli.ts Adds --profile support, skips repo-relative default surface under --profile, and threads profile into transform/emit paths.
scripts/pack-test.sh Extends packed-install smoke tests to cover JSON profiles and the installed dspack-emit bin behavior.
README.md Documents JSON profiles, --profile, loadProfile, scaffoldProfile, and CLI semantics.
package.json Bumps version to 0.4.0, copies schema into dist on build, and adds the dspack-emit bin entry.
docs/PROFILES.md Documents profile-as-data format, schema behavior, and how to use JSON profiles and scaffolding.

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

Comment thread src/cli.ts
Comment on lines +159 to +169
if (args.profile) {
try {
profile = loadProfile(JSON.parse(readFileSync(resolve(args.profile), "utf8")));
} catch (e) {
if (e instanceof ProfileLoadError) {
console.error(`error: --profile ${args.profile}: ${e.message}`);
process.exit(2);
}
throw e;
}
}
@ryandmonk
ryandmonk merged commit 156235d into main Aug 3, 2026
2 checks passed
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