Skip to content

feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had - #18319

Draft
os-warren wants to merge 13 commits into
mainfrom
claude/issue-17534-manifest-id-reverse-domain-regex
Draft

os-warren wants to merge 13 commits into
mainfrom
claude/issue-17534-manifest-id-reverse-domain-regex

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Part of #17534 — this lands the ruled change on the authoring contract, the refusal, both scaffolders and the ADR-0087 entry. The half it deliberately leaves open is named below under The one blocker, and needs the seat's word before it can be written; #17534 remains open until that half lands.

What the ruling asked for, and what is here

1. One declaration, referenced from both sites. MANIFEST_ID_PATTERN is exported from packages/spec/src/kernel/manifest.zod.ts; ManifestSchema.id and PackageSchema.manifestId both reference it. manifest.test.ts holds a per-field verdict table across the two, so a future edit to either has to break a test to drift. The .describe() moves to "must match reverse-domain notation" in the same commit as the regex — #17921's self-contained half, the wording constraint, is satisfied by construction.

2. Refusal with a remedy (#4001). The message names the key, echoes the value, shows both documented examples, and prescribes a repair it has already checked against the pattern. Built on the existing formatSuggestion from shared/suggestions.zod.ts — no new message builder.

manifest.id: Invalid package id 'blank' on `manifest.id`. Expected reverse-domain
notation ('com.steedos.crm', 'org.apache.superset') — lowercase dot-separated
segments; hyphens allowed inside a segment, underscores are not.
Did you mean 'com.example.blank'?

The suggestion arm is conditional on purpose: a bare word gets com.example.NAME; a value that already carries a dot gets its underscores hyphenated in place; a value nothing mechanical rescues gets no suggestion rather than a wrong one. Every candidate is tested against the pattern before it is offered — #17461's lesson applied to the remedy, not only to the examples.

The two @examples were verified first, as the ruling required. com.steedos.crm and org.apache.superset both match. They are no longer a comment: MANIFEST_ID_EXAMPLES is the one list the TSDoc, the refusal and the pin all read, so an example that fails its own rule is now a red test rather than bad advice.

3. Both scaffolders derive a conforming id. The bundled template ships com.example.blank, and create-objectstack interpolates deriveManifestId(projectName). os init derives from the project name too. The second half prerequisite 4 named is real and is fixed here: os init my-app sanitizes the namespace to my_app and every template interpolated that namespace into the id, producing com.example.my_app — refused by this very schema. One project name, two identifiers, contradictory rules; neither can be read off the other. Pinned from both sides against the imported MANIFEST_ID_PATTERN, never a restated regex.

4. Migration. ADR-0087 semantic entry manifest-id-reverse-domain-required — a structured TODO, no automatic rename, because the value IS the identity: the registry addresses a package by manifest_id, an installed row is keyed on it and a dependent declares it. The changeset carries the matching disposition marker and ships minor under the launch-window convention. Sub-question (1) is answered in the entry's own reason from the TSDoc's words: the rule is "unique across the entire ecosystem", so a package that is never published is still named for the ecosystem it may join.

The one blocker — measured, not assumed

The ruling states "the one non-conforming id in the tree is the scaffold itself". That is contradicted by measurement.

First, the question nobody had measured — does manifest.id reach ManifestSchema at authoring time, or only on the publish path? A failing probe, with a lit control on an existing ManifestSchema constraint:

A control  version=not-semver        REFUSED -> manifest.version: Invalid string: must match pattern
B qa id    com.dogfood.flow_fixture  ACCEPTED     (before this change)
C scaffold blank                     ACCEPTED     (before this change)

The control proves the instrument: manifest really is parsed through ManifestSchema inside defineStack(). So the answer is yes, they parse — and with the regex in place, B and C refuse.

Second, the population. Every manifest: { id: ... } literal in the tree, run through the built ManifestSchema: 21 files under packages/qa are refused, each for the same reason — an underscore in the last segment, which manifest.namespace admits and an id segment does not.

packages/qa/dogfood/test/fixtures/analytics-admission-fixture.ts:60      com.dogfood.analytics_admission
packages/qa/dogfood/test/fixtures/attachments-fixture.ts:134            com.dogfood.attachments_fixture
packages/qa/dogfood/test/fixtures/cbp-fixture.ts:48                     com.dogfood.cbp_fixture
packages/qa/dogfood/test/fixtures/comments-fixture.ts:133               com.dogfood.comments_fixture
packages/qa/dogfood/test/fixtures/email-template-materialization-fixture.ts:54  com.dogfood.email_template_fixture
packages/qa/dogfood/test/fixtures/endpoint-policy-fixture.ts:88         com.dogfood.endpoint_policy_fixture
packages/qa/dogfood/test/fixtures/flow-durable-suspend-fixture.ts:96    com.dogfood.durable_suspend
packages/qa/dogfood/test/fixtures/flow-function-effect-fixture.ts:64    com.dogfood.flow_function_effect
packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts:120            com.dogfood.runas_fixture
packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts:74              com.dogfood.flow_fixture
packages/qa/dogfood/test/fixtures/hook-runas-fixture.ts:87              com.dogfood.hookrunas_fixture
packages/qa/dogfood/test/fixtures/label-scope-fixture.ts:50             com.dogfood.label_scope
packages/qa/dogfood/test/fixtures/override-composite-fixture.ts:84      com.dogfood.override_composite
packages/qa/dogfood/test/fixtures/rls-owner-fixture.ts:52               com.dogfood.rls_fixture
packages/qa/dogfood/test/fixtures/webhook-materialization-fixture.ts:50 com.dogfood.webhook_fixture
packages/qa/dogfood/test/hook-error-format.dogfood.test.ts:50           com.dogfood.hook_error_format
packages/qa/dogfood/test/hook-refusal-user-facing-marking.dogfood.test.ts:53   com.dogfood.user_facing_marking
packages/qa/dogfood/test/registry-gate-wiring.dogfood.test.ts:73        com.dogfood.registry_gate
packages/qa/dogfood/test/sys-file-metadata-write-refusal.dogfood.test.ts:91    com.dogfood.sys_file_write_refusal
packages/qa/dogfood/test/validation-message-locale.dogfood.test.ts:42   com.dogfood.validation_message_locale
packages/qa/downstream-contract/src/stack.ts:14                         com.objectstack.downstream_contract

Each fixture calls defineStack at module top level, so the refusal is an import-time throw: seven of them were confirmed that way directly, the rest through the built schema per literal (the remaining fixtures import @objectstack/plugin-security, whose dist this measurement did not build — that is NOT MEASURED at module level, and is why the per-literal instrument is the one quoted).

They are not renamed here, and the regex is not weakened to admit underscores. packages/qa is a domain this card did not declare, and twenty fixture ids is a different PR from the one that was ruled. So this PR is red on the dogfood gate until the seat rules on the rename. Everything else is green; the gap is exactly these 21 files.

What makes the rename cheap, if it is authorized: each id occurs exactly once in the tree — its own declaration. git grep -F over all twenty-one returns one hit each, so there is no installed row, no dependencies entry and no snapshot pinning any of them. The transform is _ to - in the last segment, and the refusal already prints the target value for each one.

Two apparent counterexamples outside packages/qa are not manifest ids, and nothing needs doing about them: packages/cli/src/commands/migrate/meta.stored-flow-resolution.integration.test.ts:62 is a flow NODE id, and packages/objectql/src/plugin.integration.test.ts:259 is a plain object handed to ctx.registerService, never parsed by this schema.

In-package fixtures repaired here

The same defect existed inside the packages this card does own, and those are fixed — mechanical, single-literal, and each fixture keeps pinning exactly what it pinned before: three bare ids in packages/spec tests (p, acme), four in packages/cli tests (todo, demo, bad, a/b), and the five golden corpus entries in packages/cli/src/lint/corpus.ts. That last one is not a fixture: it is the worked-example set the metadata generator imitates, and it was teaching bare-word ids.

packages/create-objectstack/vitest.config.ts gains an anchored source alias for the one spec subpath its tests now import, because pnpm check:test-source-alias correctly refuses a pin whose verdict is a function of build state.

Not touched, deliberately

docs/adr/0087-metadata-protocol-upgrade-contract.md — the registration this gate wants is the changeset's disposition marker plus the entry file, both present. Editing the ADR would make this a governed-surface diff for no gain. Also untouched: every file in open PR #18198, packages/spec/src/shared/retired-key.ts, packages/spec/src/ui/chart.zod.ts.

Verification

Run at d0d859edbc, the final commit.

what result
node scripts/pm/dispatch-gates.mjs --ran 127 derived families, 127 run, 0 NOT-MEASURED, 0 UNRUN, every one recorded with its exit code, all zero
pnpm --filter @objectstack/spec test 480 files, 13715 passed, 1 skipped
pnpm --filter @objectstack/spec typecheck pass
pnpm --filter @objectstack/cli exec vitest run --project unit 209 files, 2988 passed
pnpm --filter @objectstack/cli typecheck pass
pnpm --filter create-objectstack test 16 files, 215 passed
pnpm --filter create-objectstack typecheck pass
pnpm lint (repo-wide, eslint . --no-inline-config) exit 0 — the full population, not a narrowing
pnpm --filter @objectstack/spec check:generated all 15 artifacts current after regenerating api-surface, export-origins, content/docs/references
packages/qa dogfood tier NOT RUN — known red, the 21 files above

packages/cli's integration tier is declared to CI: the diff touches no spawn entry, no bin/, no test/helpers/serve-process.ts and no driver or kernel boot path.


Generated by Claude Code

@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 15, 2026
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/cli, create-objectstack, objectstack-blank, @objectstack/plugin-dev, @objectstack/downstream-contract, @objectstack/spec, touching 25 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/plugins/plugin-dev/src/index.ts, packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 500c1b56956537dd13dc6b2129dd2fb428da351c.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/plugins/plugin-dev/src/index.ts, packages/spec/api-surface/kernel.json, packages/spec/export-origins/kernel.json) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 500c1b56956537dd13dc6b2129dd2fb428da351cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from f38d916596beb104ab477b101d42404e709525dd — the merge of head e9d4431a2d8f64c2cbc63caa299912dd154a6543 into base 500c1b56956537dd13dc6b2129dd2fb428da351c, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f38d916596beb104ab477b101d42404e709525dd && git checkout f38d916596beb104ab477b101d42404e709525dd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 500c1b56956537dd13dc6b2129dd2fb428da351c e9d4431a2d8f64c2cbc63caa299912dd154a6543 && git checkout -B drift-repro 500c1b56956537dd13dc6b2129dd2fb428da351c && git merge --no-ff e9d4431a2d8f64c2cbc63caa299912dd154a6543

node scripts/docs-audit/affected-docs.mjs --json 500c1b56956537dd13dc6b2129dd2fb428da351c

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 500c1b56956537dd13dc6b2129dd2fb428da351c → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-warren os-warren added the domain:spec label Sep 15, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

This PR is red on purpose, and it is blocked on one maintainer decision — not on an unhandled defect

What is failing, read from GET /commits/d0d859edbcfcf85240665f13207670f304ed5311/check-runs at 2026-09-15T17:16:22Z: 43 check runs, 26 success, 6 skipped, 11 failingTest Core and its six shards, Dogfood Regression Gate and its three shards.

Why. The card's ruling adopts PackageSchema.manifestId's regex /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/ for ManifestSchema.id, on the stated premise that 「the one non-conforming id in the tree is the scaffold itself」. That premise is false, and the implementer measured it rather than working around it. Run through the BUILT ManifestSchema at this head, 26 in-tree manifest id literals are refused: 21 real (all under packages/qa, every one failing for an underscore in the last segment — com.dogfood.flow_fixture, com.objectstack.downstream_contract and their siblings), 3 are os init template strings now conforming by construction, and 2 are false positives (a flow node id and a registerService mock).

The reachability question that decides whether those 21 matter was answered by a failing probe with a lit control, before the change:

A  control  version=not-semver          → REFUSED (must match pattern)
B  qa id    com.dogfood.flow_fixture    → ACCEPTED
C  scaffold blank                       → ACCEPTED

So manifest.id does reach ManifestSchema at defineStack() time, and the 21 are real refusals rather than a theoretical concern.

Why nothing was done about them. The dispatch order for this card was explicit: if the 21 turned out to be real, stop and report — ⛔ do not rewrite twenty-one fixture ids on an implementer's own authority, and ⛔ do not weaken the regex. Both prohibitions still hold. Weakening is additionally refused by the ruling itself, which requires the pattern 「declared once and referenced from both sites so the two cannot drift again」; admitting underscores on one side re-opens exactly the drift the card exists to close. packages/qa is also a domain this card did not declare in its claim.

What is needed. One decision, and it is the maintainer's because the ruling's own population statement is what turned out to be wrong:

  • A — authorize the 21 renames inside this PR: one mechanical edit per file, underscore to hyphen in the last segment. Measured cost: each id occurs exactly once in the whole tree, at its own declaration, so nothing else addresses the old value, and the refusal message already prints the target. The PR grows by 21 single-line edits into an undeclared domain.
  • B — split: a sibling card renames the 21 first, this PR lands after. Cost: two PRs, an ordering dependency no gate enforces, and this PR sits red until then.

The decision is recorded on the card at #17534 and is with the maintainer now. This PR stays draft with no auto-merge and no enqueue until it is answered; the rest of the lane's queue keeps moving.

Not blocked, and already fixed in this PR — a second premise that turned out to be real: every os init template interpolated the snake_case namespace into the id, so os init my-app wrote com.example.my_app, which this schema refuses. Landing the regex without that fix would have shipped a scaffolder whose own output fails its own first os validate.


Generated by Claude Code

…omain manifest ids

`manifest.id` now enforces the reverse-domain rule, and 21 fixture
declarations under `packages/qa` carried an underscore in their last
segment. 20 of them are renamed here, underscore to hyphen, exactly the
repair the refusal message itself suggests.

The 21st, `com.dogfood.flow_fixture` in
`packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts`, is left alone:
it is not a single-occurrence declaration. The same literal appears twice
more in `packages/spec/src/kernel/manifest.test.ts`, as the sample input
for the "repairs a dotted value in place" refusal-message assertions. The
rename mandate covers single-occurrence declarations, so that one is
reported rather than swept. `test/flow-node.dogfood.test.ts` therefore
still refuses at boot.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
`com.dogfood.flow_fixture` was held back in the previous commit because
the literal also appears twice in `packages/spec/src/kernel/manifest.test.ts`,
so it was not a single-occurrence declaration and the mechanical rename
mandate did not obviously cover it.

Those two hits are deliberate NEGATIVE TEST INPUT, not references to this
fixture: line 643 asserts the refusal for that value suggests
`com.dogfood.flow-fixture`, and line 653 feeds it alongside `blank`,
`my_app` and `support_desk` as a batch of ids the schema must refuse.
They are left exactly as they are — renaming them would delete the test
of the rule this card adds.

The new id is the one the refusal itself prescribes for this value, the
same underscore-to-hyphen repair the other 20 took. `manifest.test.ts`
stays green (66 passed) with the fixture renamed, which is what shows the
two are independent.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Status refresh — the blocker changed, and my earlier comment on this PR is now the wrong description of it

My comment 5684753340 said this PR was held on one question: whether to rename 21 packages/qa ids. The maintainer ruled A (rename them here), the implementer did, and f983362592 completed the twenty-first. Test Core stayed red anyway, so the population was re-measured — and 21 was not it either.

Measured at head f983362592, by two lexically unambiguous passes (the id inside manifest: { on the same line, or on the line immediately after, so nested id keys on navigation items, state machines, view probes and migration entries cannot contaminate the count): 67 manifest ids are refused by the ruled regex, across eight packagespackages/cli 45, packages/runtime 8, packages/plugins 6, packages/spec 3, packages/qa 2, packages/objectql 1, packages/lint 1, packages/rest 1. Both numbers are floors: an id written across three or more lines is invisible to both passes.

The fact that decides how serious this is: of the 63 from the first pass, 62 are in a .test.ts file or under a /test/ path, and the single exception is a docblock example (packages/plugins/plugin-dev/src/index.ts — prose in a comment, not executed). So no shipped, executable manifest id in this repository is refused. The contract change is sound; what was under-estimated is the fixture churn it costs.

Current CI, head f983362592, run 35036842827 still in_progress at 2026-09-15T23:54:42Z: 37 runs — 26 success, 3 skipped, 3 failing (Test Core 1/6, 2/6, 4/6), 5 still going. Down from 11 failing two heads ago; the remainder are the un-renamed fixtures above.

Why nothing further is being pushed. Renaming ~46 more fixtures across seven additional packages is a scope decision, not a repair — and an hour ago I wrongly told the implementer to fold two packages/cli fixtures in as "the same class its predecessor already handled". With eight packages in play that framing does not hold, and I have retracted it. ⛔ No further renames on seat authority. The implementer has been stopped and is reporting needs_decision.

The full measurement and the question now with the maintainer are on the card: #17534 (comment)

This PR stays draft, with no auto-merge and no enqueue, until that is answered. The 21 ruled renames stay as they are — they are correct and they are landed on this branch.


Generated by Claude Code

…pellings

The maintainer ruled that the fixtures outside `packages/qa` are renamed in
this PR too. Each new id is the one the schema's own refusal prescribes for
the old value, so the tree and the diagnostic agree.

Renamed (18 ids, 16 files): `packages/cli` 14, `packages/lint` 3,
`packages/metadata` 1, `packages/plugins/plugin-dev` 3 (see below), plus the
docblock noted last.

Three of these were NOT single-literal swaps, and are called out because a
naive rename would have silently deleted what the test pins:

  * `dev-i18n-packages-reader.test.ts` builds a dependency CYCLE between two
    packages, and the `dependencies` keys address the ids BY NAME. The ids and
    both dependency keys are renamed together; renaming only the ids would
    have dissolved the cycle and left the test asserting nothing. It still
    reports `Circular dependency detected`.
  * `metadata-type-schema-gate.test.ts` and `format-zod-union.test.ts` and
    `authoring-rule-command-parity.test.ts` each carry a `namespace` that
    repeats the old id, and the first also carries a route path
    `/api/v1/apps/gate_probe/things`. Only `manifest.id` is renamed —
    `namespace` has its own rule and the path pairs with it.
  * `plugin-dev/src/index.ts` is a DOCBLOCK example, not a fixture: prose
    teaching `manifest: { id: 'my-app' }`, an id its own schema now refuses.
    It cannot red a test; it is corrected because it is published teaching.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…ges/qa

19 more ids, each the target the schema's own refusal prescribes for the old
value, rewritten at the exact offset of the `manifest.id` literal so that a
`namespace`, a `name` or a route path repeating the old spelling is left alone.

Scope of THIS commit is deliberately the single-occurrence ones. Every id here
occurs exactly once in its own file, so the rename cannot silently unhook an
assertion that addresses the old value.

Not included, and reported rather than guessed:

  * 15 ids whose literal occurs MORE than once in its own file, so renaming
    the declaration alone would leave assertions naming the old value —
    `pkg-a` (7 occurrences), `test` (6), `a` (6), `my-app` (5), `demo` (3) and
    ten more. These need a coordinated rename of the id and every reference,
    which is reading work per file, not a mechanical swap.
  * `com.test.14397` — the schema offers NO suggestion for it, because the
    failing segment starts with a digit and no mechanical repair exists.
  * `artifact-granted-permissions.test.ts` — not a rename at all: it pins the
    ADR-0130 D4 door ORDER, that `''` passes the schema and is caught one door
    later. The new rule moves the refusal to door 1, so no conforming id can
    preserve what the test pins.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
The last 16, each read before it was touched. Every replacement is anchored on
the `id:` key, so a `name`, a `namespace` or a field named the same is left
alone, and each file's occurrence count was asserted before and after.

Reading first is what made these safe, and two would have gone wrong without it:

  * `artifact-collections.test.ts` has `'a'` six times, but only two are the
    package id — the other four are a FIELD named `a`
    (`{ a: { name: 'a', type: 'text' } }`). A whole-file swap would have
    renamed a field the assertions address.
  * `package-registry.test.ts` and `lint-protocol-range.test.ts` repeat the old
    id as a `namespace`, and `serve-host-config.test.ts`,
    `metadata-collection.test.ts` and others repeat it as a free-form `name`.
    `namespace` has its own rule and `name` has none; neither is renamed.

Where the repeat WAS a real reference it moved with the id: all seven `pkg-a`
in `packages-capability-gate.test.ts` (package record ids and manifest ids in
the same mock), both in `packages-uninstall-envelope.test.ts`, both `test-app`
registrations in objectql, and both `locale-producer-app` sites in runtime.

`com.test.14397` becomes `com.test.card-14397`: the schema offers no suggestion
for it, because the last segment starts with a digit, so the name was ruled
rather than derived. `ManifestSchema` confirms the replacement parses.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
…tree

The os-regen merge sequence's collection commit. `api-surface/kernel.json` and
`export-origins/kernel.json` were edited on BOTH sides, so the merge driver ran,
exited 0 and silently kept one side; step 2 took main's side and this commit
carries the regeneration from the merged tree, built first so api-surface reads
a dist that matches src rather than reporting phantom removals.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation domain:spec needs:contract-review size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants