Skip to content

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

Merged
os-zhuang merged 21 commits into
mainfrom
claude/issue-17534-manifest-id-reverse-domain-regex
Sep 21, 2026
Merged

os-zhuang merged 21 commits into
mainfrom
claude/issue-17534-manifest-id-reverse-domain-regex

Conversation

@os-warren

@os-warren os-warren commented Sep 15, 2026

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, plus (2026-09-20, under ruling batch #200 item 3) the merge of origin/main and ruling Q3-A component 2: the ADR-0130 D4 record-keeping block. ⚠️ The PR is therefore a GOVERNED diff (docs/adr/**, tier H, 1 of 95 paths, under the 5000-line line): it lands by the maintainer's own merge or an authorized APPROVED review from a GOVERNED_APPROVERS account, after the at-tier contract review. Part of, not Fixes: #17534 closes when the seat closes it.

Clause-②: yes — manifest.id's accept set narrows (the reverse-domain pattern is enforced where a bare string was accepted); the carrier of record is on the card (5691010940).

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 former blocker — landed, gate green

The 21 packages/qa manifest ids (and, after the maintainer widened the scope on 2026-09-15, the further fixtures across packages/cli, runtime, plugins, spec, objectql, lint, rest) were renamed inside this PR under the 2026-09-15 rulings on #17534; at head 1d4c99bcbd (the F1 rounds after the at-tier reviews 5751237820 and 5751666135; round 3 PASS 5751970203) zero literal manifest.id values the pattern refuses remain in the tree outside the declared residues — the install-door residual pin packages/runtime/src/domain-handler-registry.test.ts:591 (pkg-a, kept on purpose: it pins the HTTP install door that still reads the id positionally), this PR's own changeset FROM → TO examples (a migration note must show the refused spelling), and the spec template-manifest negative controls — measured by the round-3 reviewer's eight-leg sweep (brace-key, JSON key, window, manifestId, const manifest, aliased body, id: CONSTANT, factory body, plus an inverse constant leg) over 9070 tracked files through the BUILT ManifestSchema.shape.id with a 12-shape injected control, 12/12 caught (record 5751970203; the dev's own sweeps are on #17534, 5751523635 and the round after it); the Dogfood Regression Gate (1/3, 2/3, 3/3) is green on CI. The 2026-09-20 merge had carried 13 crm_core / crm_service ids from main into packages/lint/src/validate-translation-references.test.ts, and seven published guidance sites (packages/spec/prompts/create-new-project.md, five content/docs pages, service-package/README.md) plus two platform-checklist items and one create-objectstack fixture were still teaching refused ids; all renamed in the F1 round (44 occurrences over 10 files). The merge of origin/main (2026-09-20, scripts/pm/os-regen-merge.sh, merge commit only) forced two pin repairs — packages/spec/src/api/package-api.test.ts and packages/cli/src/utils/nav-contribution-groups.package-id.test.ts landed on main while this branch was open and measured the accept set this branch narrows; each is rewritten to the measured reality with a lit control, MANIFEST_ID_PATTERN untouched. The ADR-0130 D4 block records the entry door order (DOOR 1, the schema, refuses an empty manifest.id as INVALID_ARTIFACT_PACKAGE_ENTRY / 422; DOOR 2, artifactPackageId, never runs for that spelling), cites MANIFEST_ID_PATTERN and PackageSchema.manifestId, and names the fail-OPEN → fail-CLOSED direction and its population — an addition to D4, not a reversal.

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 original round). Re-run at the final head 6783718d6a (2026-09-20): 137 derived gate families, 137 run, 0 NOT-MEASURED; runtime 270 files / 3734 passed; spec 502 files / 14733 passed, typecheck and check:generated (15/15) green; cli unit 221 files / 3127 passed and integration shards 26/207 + 25/220 passed; create-objectstack 16 files / 215 passed; pnpm lint repo-wide exit 0; ADR anchor/symbol/link gates and check-adr-0087-registration --base origin/main exit 0. The readings of record are in the os-dev-report on #17534 (5751052645).

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.

Original author: Claude Code session session_01KB5PFtxuy1x3dcR5gxudx6 (the implementation rounds); merge, ADR-0130 D4 block and pin repairs by the director seat's round, session_012GcsUbuqFGBibkEDMRC1eE.


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 7 package(s): @objectstack/cli, create-objectstack, objectstack-blank, @objectstack/plugin-dev, @objectstack/downstream-contract, @objectstack/service-package, @objectstack/spec, touching 25 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/plugins/plugin-dev/src/index.ts, packages/services/service-package/README.md, packages/spec/api-surface/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 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2.

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

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/plugins/plugin-dev/src/index.ts, packages/services/service-package/README.md, packages/spec/api-surface/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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2packageMentionDocs.

Which tree this was computed on

This run read content/docs from cbdc39434111fc2fdbeeb4b9843938ee653edc30 — the merge of head 1d4c99bcbdf082c937d06157d488d8ac191939b6 into base 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2, 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 cbdc39434111fc2fdbeeb4b9843938ee653edc30 && git checkout cbdc39434111fc2fdbeeb4b9843938ee653edc30
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2 1d4c99bcbdf082c937d06157d488d8ac191939b6 && git checkout -B drift-repro 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2 && git merge --no-ff 1d4c99bcbdf082c937d06157d488d8ac191939b6

node scripts/docs-audit/affected-docs.mjs --json 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2

⚠️ 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 5a5e710fb9b5bd973d44f9a5d3466d94ddaf4fa2 → 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>
…s on the merged tree

The merge of origin/main routed six generator-owned artifacts through the
os-regen driver, which exits 0 while keeping one side. Regenerated from the
merged sources on a committed base, so main's additions and this branch's
`MANIFEST_ID_PATTERN` / `MANIFEST_ID_EXAMPLES` entries are both present.

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

Ruling option A, component 2. `ManifestSchema.id` now carries
`MANIFEST_ID_PATTERN`, and `AssembledPackageBodySchema` inherits it, so a
`packages[]` entry whose `manifest.id` is empty is refused at the schema door —
before `artifactPackageId`'s `id || name` fallback is consulted. D4 states that
order, cites the pattern, and records the direction the change reverses on a
consent path: what used to leave a `''`-keyed consent record unbound with the
package loaded anyway (fail-OPEN) now refuses the artifact outright
(fail-CLOSED). D4's two branches are unchanged; this is an addition to the
record, not a reversal of a decision.

Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>
`origin/main` landed `packages/spec/src/api/package-api.test.ts` while this
branch was open, and two of its cases measured the accept set this branch
narrows: the control that completes the domain-handler-registry drive (`pkg-a`
is not reverse-domain notation, so completing the missing keys no longer makes
it parse) and the residual that ran the other way (a whitespace-only `id` used
to parse here while the door answered 400). Both are rewritten to what is now
measured, each with a lit control proving the id is what decides it. No
behaviour moves and the pattern is not relaxed.

Claude-Session: https://claude.ai/code/session_012GcsUbuqFGBibkEDMRC1eE
Co-authored-by: Claude <noreply@anthropic.com>
CI on this branch's merge head reds `packages/cli`'s integration tier: the
#18490 file composes an artifact whose `manifest.id` is `''` and parses it
through `ObjectStackDefinitionSchema`, which this branch's pattern now refuses.
That file said it would red first and that the pins under it would then measure
nothing, so the red is converted into the pin it asked for: the first case now
pins the refusal itself, on `manifest.id`, with a lit control proving the id is
what decides it, and the three cross-door cases keep measuring that the build
names a package exactly as the runtime fold does at an identity a command can
actually hand down. The divergence the file was written around is structurally
closed — a parsed package's id is non-empty by construction, so the owner's
`id || name` never falls back. The degenerate fixture is not kept alive by
bypassing the parse.

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

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6783718d6ac5a39c79fc32637e8f03b74fd526fa

Isolated at-tier reviewer, spawned for this head alone; reviewed 2026-09-20T16:56Z. Bases: merge-base 8271c814253 (= the merge's second parent); origin/main tip during the review fade3da23eb (9 commits past the merge-base, none touching the PR's 95 paths); one detached worktree ../objectstack-review-18319 at 6783718d6a (fresh pnpm install, @objectstack/spec + the cli/runtime/create-objectstack closures built there) and one throwaway driverless bare clone for the merge probe — both removed at the end. Inputs: card #17534 (body + all 18 comments) and the PR (body, 95-file diff, check-runs); every PR-body claim and every os-dev-report figure was re-measured, not reused. Exit codes captured by redirect-then-$?; every instrument was self-tested on a known input first and every zero below has a lit control.

① Derived judgments

  1. One declaration, two references. MANIFEST_ID_PATTERN is declared once (packages/spec/src/kernel/manifest.zod.ts:262); ManifestSchema.id (:351) and PackageSchema.manifestId (packages/spec/src/marketplace/package.zod.ts:174) both call .regex(MANIFEST_ID_PATTERN, …). The regex literal occurs in the three-dot diff exactly twice — + the declaration, the old copy in package.zod.ts, byte-identical text — and once in the tree at HEAD; no other copy, nothing weakened. Probe on the BUILT dist, per field, both sites: com.steedos.crm accept/accept, org.apache.superset accept/accept, blank refuse/refuse, com.example.my_app refuse/refuse, '' refuse/refuse; control: a fresh literal copy accepts/refuses the same way. (One probe assertion of mine failed — cross-bundle object identity between dist/index.mjs and dist/kernel/index.mjs — the root entry does not export these symbols at all; api-surface/kernel.json is the surface, +3 there, root surface unchanged. Probe artefact, not a defect.) RIGHT.
  2. Refusal with a remedy. Names the key (manifest.id / manifestId), echoes the value, shows both examples; arms measured on the built schema: bare word → com.example.NAME (blankcom.example.blank); bare namespace-shaped → hyphenated (my_appcom.example.my-app, com.example.my_app never offered); dotted with underscores → hyphenated in place (com.dogfood.flow_fixturecom.dogfood.flow-fixture); Com.Example.App, '', ' ', com., 2app → no suggestion. Every suggestion offered over 17 inputs itself parses green. RIGHT.
  3. MANIFEST_ID_EXAMPLES = ['com.steedos.crm','org.apache.superset'], both match the pattern; read by the TSDoc @examples, by manifestIdRefusal and by the pin (manifest.test.ts, per-field verdict table across both schemas). RIGHT.
  4. Both scaffolders derive a conforming id. create-objectstack: deriveManifestId (rewrite-identity.ts) — my-appcom.example.my-app, my_appcom.example.my-app, ''com.example.app, 123com.example.app-123; the bundled template ships com.example.blank; pinned against the imported MANIFEST_ID_PATTERN via an anchored source alias. os init: manifestIdSlug in all three templates (init.ts:626/720/798), the my-app → namespace my_app → id com.example.my-app case pinned in packages/cli/test/init-manifest-id.test.ts through both the pattern and ManifestSchema.safeParse. Two copies of the slug logic live in two published packages (cli, create-objectstack) — noted, not a contract defect. Suites here: pnpm --filter create-objectstack test 16 files / 215 passed; pnpm --filter @objectstack/cli exec vitest run --project unit 221 files / 3127 passed (init-manifest-id.test.ts inside) — exit 0 each. RIGHT.
  5. ADR-0087 semantic entry manifest-id-reverse-domain-required present as entries/semantic/18.manifest-id-reverse-domain-required.ts + the generated region of registry.ts (check:migration-registry green); the changeset carries the ADR-0087 disposition marker (an HTML comment reading adr-0087: registered manifest-id-reverse-domain-required); check-adr-0087-registration --base origin/main exit 0 ([BREAKING+bang] registered … new here). RIGHT.
  6. Fixture renames — the "zero literal refused manifest.id values remain in the tree" claim. Two independent extractions over the whole tree at HEAD (bounded-brace manifest: { … id: '…' }: 363 literals; 3-line window after manifest: {: 421 literals), each literal run through the BUILT ManifestSchema.shape.id; control: an injected control_bad_id lit both. Result: every executed fixture that reaches ManifestSchema is conforming (the refused hits there are the four intentional negatives — package-api.test.ts:914 ' ', artifact-granted-permissions.test.ts:303 '', package-publish-manifest-id.test.ts:329 local.2024-app, plus the it.each negatives in manifest.test.ts) and the dogfood / spec / runtime / cli suites are green. But the tree-wide zero is false at this head — residue, none of it reaching the schema: (a) packages/lint/src/validate-translation-references.test.ts — 13 id: 'crm_core' / 'crm_service' manifest literals handed straight to validateTranslationReferences (main-authored lint: translation-target-unknown reads apps[].navigation only, so every locale key for a CONTRIBUTED navigation item (navigationContributions) is a false positive whose advice deletes a translation the runtime honours #18203/fix(lint): the translation rule's OBJECT rung reads what the artifact's packages[] provide #19347, entered this branch through the 2026-09-20 merge; 0 at the pre-merge head 4f277510f3, 13 at the merged-in tip 8271c81), i.e. fixtures inside the population the 2026-09-15 ruling named 「剩下那 … 夹具一起改,在这个 PR 里」; (b) published guidance teaching a spelling the schema now refuses: packages/spec/prompts/create-new-project.md:82 com.example.my_erp (ships in the @objectstack/spec tarball, files: prompts), content/docs/getting-started/your-first-project.mdx:112 my-app, content/docs/api/declarative-endpoints.mdx:50 and content/docs/protocol/kernel/http-protocol.mdx:1182 acme-crm (their os:check is tsc-only, so no gate sees a regex verdict), content/docs/api/metadata-api.mdx:108-117 plugin-auth ×4, packages/services/service-package/README.md:41 crm, docs/qa/platform-checklist/areas/{api-backend,platform-core}.json qa_pkg_lifecycle_probe / qa_manifest_probe_a. packages/runtime/src/domain-handler-registry.test.ts:591 pkg-a also remains and must stay — it is the door residual's own pin (③). Template-interpolated ids (com.example.${ns}, pkg.of.${label}, the three init.ts templates) classified separately: all conforming by construction. ⇒ RIGHT for the schema-reaching population, WRONG as stated — see ⛔ F1.
  7. ADR-0130 D4 block. Door order verified against the code: DOOR 1 = ArtifactPackageSchema.safeParse(entry) at packages/core/src/artifact-packages.ts:224refuse('INVALID_ARTIFACT_PACKAGE_ENTRY', …) with status: 422 (:152-157), the issue path manifest.id and the echoed '' (probe: ArtifactPackageSchema refuses {manifest:{id:''…}} at path manifest.id; control entry parses); DOOR 2 = artifactPackageId (:172, still id || name, untouched by the diff) is consulted only at :251, after DOOR 1 threw — unreachable for '' and for { id: '', name: 'x' }; the reach is carriedPackageIds (runtime/…/artifact-granted-permissions.ts:131) → resolveArtifactPackageOrder. The block is +38/−0 inside D4 after the reservation paragraph; D4's two branches (packages[] additive, single-manifest returned by reference) are untouched — an addition, not a reversal, so no superseding ADR is owed. check:adr-anchors, check:adr-symbol-anchors (2113 anchors resolve), check:adr-links all exit 0 after their self-tests. RIGHT.
  8. The two pins the merge forced. packages/spec/src/api/package-api.test.ts: registryKeysCompleted (pkg-a) now refused, lit control id: 'com.acme.pkg-a' parses; whitespace-only id refused, lit control SDK_MANIFEST parses — both re-measured on the built dist. packages/cli/src/utils/nav-contribution-groups.package-id.test.ts: composedArtifact(GROUP, '') refused at packages.0.manifest.id with Invalid package id '', lit control ORDERS_ID parses and both packages are named; the cross-door cases keep their property at a reachable identity. Pattern not weakened anywhere (item 1). Runs here: pnpm --filter @objectstack/spec test 503 files / 14735 passed, exit 0; the cli pin is integration tier, run alone on that tier: 1 file / 4 passed, exit 0. RIGHT.
  9. Merge faithfulness. git diff origin/main...6783718d6a = 95 paths = the PR's file list exactly. Both merge commits re-derived in a driverless bare clone (git clone --bare --shared + merge-tree --write-tree of each merge's parents, no merge=os-regen driver): merge 1 (58acd78539) equals its regeneration commit e9d4431a2d byte-for-byte; merge 2 (ceb042b070) differs from its regeneration commit 7030cf6f9a only in content/docs/references/api/package-api.mdx, two generated rows that carry the new .describe() text — generator output, not a lost hunk. pnpm --filter @objectstack/spec check:generated → exit 0, 15/15 artifacts current, tree clean afterwards. packages/spec/api-surface-declarations/ absent at HEAD (main's deletion won). No main change lost. RIGHT.
  10. Governed reading. check-governed-merges.mjs --branch HEAD → exit 3, GOVERNED, tier H, docs/adr/** ×1 of 95, 1217 changed lines ≤ 5000 — matches the PR body. Draft, no auto-merge, no queue. RIGHT.

Also run in the worktree, every build/test through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 read from the wrapper each time): cli/runtime/create-objectstack dependency closures built; pnpm --filter @objectstack/spec test 503 / 14735 passed; pnpm --filter @objectstack/spec typecheck exit 0 (its check:test-typecheck leg compiles the test layer); pnpm --filter @objectstack/runtime test 270 files / 3734 passed, 1 skipped (3735); pnpm --filter @objectstack/cli exec vitest run --project unit 221 / 3127 passed; pnpm --filter create-objectstack test 16 / 215 passed; pnpm lint (repo-wide, eslint . --no-inline-config) exit 0; check:adr-anchors (106 self-test assertions), check:adr-symbol-anchors, check:adr-links exit 0; pnpm --filter @objectstack/spec check:generated exit 0, 15/15. NOT MEASURED here: the packages/qa dogfood tier and the full cli integration shards (CI carries both, green on this head); check:react-declaration-parity (needs the objectui manifest input, as check:generated itself reports).

② Semver level

.changeset/17534-manifest-id-reverse-domain.md: @objectstack/spec, @objectstack/cli, create-objectstack at minor; a feat(spec)!: title, a BREAKING banner stated in the accept-set sense with the launch-window minor rationale, the FROM → TO mapping with the one-line fix, the ADR-0087 marker, the identity/republish warning, and the 「fail-OPEN → fail-CLOSED」 section naming the direction (consent/permission path), the population (artifacts with manifest.id: '', already unpublishable and already unbound) and the provenance (the package door, not the enforcer). Gates on the worktree, self-tests first: check-changeset-no-major --base origin/main exit 0 (no major; its level axis is PR-payload-only and is green as Check Changeset on this head), check-adr-0087-registration --base origin/main exit 0, check-empty-changeset 0, check-changeset-fixed 0. Clause-②: yes on both carriers (card 5681542663 as governing claim, PR body) — three new public symbols on @objectstack/spec/kernel (MANIFEST_ID_PATTERN, MANIFEST_ID_EXAMPLES, manifestIdRefusal, api-surface/kernel.json +3) — so minor is the floor and the level is consistent. The banner is spelled 「BREAKING in the accept-set sense」 rather than the sibling changesets' 「BREAKING for authored metadata」 (#15646, #18012, #19046) — the gate classifies it [BREAKING+bang] either way; a wording variance, not a defect. @objectstack/runtime and plugin-dev ship no source change (tests and a docblock only), so their absence from the changeset is right. RIGHT.

③ Boundary flags

  • OQ1 (PR-body PATCH): resolved by the seat — the body at review time carries the landed state (blocker section replaced, governed note, 6783718d6a verification, Clause-②: yes). RIGHT, with one correction now owed: the sentence 「zero literal refused manifest.id values remain in the tree」 is false at this head (①6).
  • OQ2 (check-clause2-carriers --pair 18319 exit 4, row C9): measured now → exit 0. The Release: line 5751072453 is read by the checker as retracting the same-author claim 5750574811 (not as releasing 5681542663), so the governing claim is os-warren's 5681542663 — both carriers still declare yes, the pair is legible. RIGHT (option A taken; attribution quirk is the checker's, non-blocking).
  • Class (c) candidate — the runtime install door reads manifest.id positionally: CONFIRMED. packages/runtime/src/domains/packages.ts:755-760const manifest = body.manifest || body; const pkgId = typeof manifest?.id === 'string' ? manifest.id.trim() : ''400 only when empty; :804 parses ManifestSchema.shape.version alone ([finding] POST /api/v1/packages installs a manifest with NO version and answers 201, while its published declaration requires one — the door parses nothing #19120, scoped by its own docblock to the version leg); PackageInstallBodySchema.safeParse(body) is never called. So POST /packages with id: 'pkg-a' answers 201 (pinned at packages/runtime/src/domain-handler-registry.test.ts:591 and by the [finding] PackageInstallRequestSchema is a published, route-bound request contract that no layer parses — the install door is declared and enforced nowhere (ADR-0049) #18058 SUBSET pins in package-api.test.ts:890) while defineStack, os build and the publish face refuse it. Pre-existing, documented, HTTP-door-only; this PR widens the residual class. Does not block this PR — the remedy is the door parsing the leg its own declaration already carries (contract-first, a separate card); escalate to triage for filing, carrier [finding] PackageInstallRequestSchema is a published, route-bound request contract that no layer parses — the install door is declared and enforced nowhere (ADR-0049) #18058. RIGHT.
  • Dead-code note (DOOR 2's no usable package id unreachable from packages[]): confirmed by reading; recorded in the ADR block; artifactPackageId has other callers. RIGHT, correctly not filed.
  • STALE TREE / os-regen-merge.sh exit-1 notes: informational, consistent with what I saw. RIGHT.
  • not_measured (CI convergence): converged — at 2026-09-20T16:55Z the head reads 51 success / 9 skipped / 0 failure / 0 in progress; all seven required contexts are success (Lint & Repo Gates completed 2026-09-20T16:46Z, TypeScript Type Check, Test Core 1-6, Dogfood Regression Gate 1-3, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). RIGHT now.
  • Reviewer's own flag → ⛔ F1. The residue in ①6: 13 refused fixture ids inside the ruled population (packages/lint/src/validate-translation-references.test.ts, arrived via the merge) and published guidance in packages/spec itself (prompts/create-new-project.md, in the tarball) plus five docs/README pages teaching ids the schema now refuses — the exact 「AI 拿到一个成功响应」 trap the card was filed for, now on the package that refuses it. All on non-governed paths, ~25 mechanical lines, no tier change. Either rename them in this PR under the 2026-09-15 fixtures ruling and correct the PR-body sentence, or the maintainer strikes the docs half explicitly (as was allowed for the plugin-dev docblock) — but the claim as written cannot stand as the review record.

Implemented-by: claude/issue-17534-manifest-id-reverse-domain-regex
Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE

VERDICT: FAIL

F1 — ①6/③: the PR-body claim 「zero literal refused manifest.id values remain in the tree」 is falsified at 6783718d6a: 13 non-conforming fixture ids in packages/lint/src/validate-translation-references.test.ts (inside the ruled rename population, merged in from main) and published guidance teaching refused ids (packages/spec/prompts/create-new-project.md:82 com.example.my_erp; content/docs/getting-started/your-first-project.mdx:112 my-app; content/docs/api/declarative-endpoints.mdx:50, content/docs/protocol/kernel/http-protocol.mdx:1182 acme-crm; content/docs/api/metadata-api.mdx:108-117 plugin-auth; packages/services/service-package/README.md:41 crm; two docs/qa/platform-checklist items). Everything else in ①–③ reads RIGHT; the ruled contract (pattern, refusal, examples, scaffolders, ADR-0087 entry, ADR-0130 D4 block, the two pins, merge faithfulness, changeset) is delivered.


Generated by Claude Code

… to reverse-domain

`ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, so every remaining literal
`manifest.id` in the tree that the pattern refuses is either a fixture that would
not survive its own schema or published guidance teaching an author a spelling the
runtime rejects. Both are renamed here; nothing about the pattern moves.

Fixtures (no assertion reads the id string, so each case keeps its meaning):
- packages/lint/src/validate-translation-references.test.ts — the 13
  `crm_core` / `crm_service` package ids become `com.example.crm-core` /
  `com.example.crm-service` (these arrived from `main` through the merge).
- packages/create-objectstack/src/rewrite-identity.test.ts — the scaffolded
  config fixture's `id: 'x'` becomes `com.example.x`, the spelling its own
  sibling fixture in the same file already used.

Published guidance:
- packages/spec/prompts/create-new-project.md — `com.example.my_erp` →
  `com.example.my-erp` (this file ships in the spec tarball).
- content/docs/getting-started/your-first-project.mdx — `my-app` →
  `com.example.my-app`, which is what `create-objectstack` now derives for a
  project named `my-app`; `namespace: 'my_app'` is untouched (a namespace admits
  underscores and this key does not).
- content/docs/api/declarative-endpoints.mdx, content/docs/protocol/kernel/http-protocol.mdx
  — `acme-crm` → `com.acme.crm`; `namespace: 'acme'` untouched.
- content/docs/api/metadata-api.mdx — `plugin-auth` → `com.objectstack.plugin-auth`
  on both request bodies and both echoed responses.
- packages/services/service-package/README.md — `crm` → `com.example.crm`,
  including the `get`/`delete` calls that address the same package.
- docs/qa/platform-checklist/areas/{api-backend,platform-core}.json — the scratch
  probe ids the two install-door items POST move to reverse-domain spellings;
  both items bump `revision` and append the history entry that edit owes.

Deliberately NOT renamed, each for a stated reason:
- packages/runtime/src/domain-handler-registry.test.ts:591 `pkg-a` — it pins the
  HTTP install door's residual, which answers 201 to an id every other door now
  refuses; renaming it would delete the only pin on that gap.
- the `FROM` examples in this PR's own changeset — showing the refused spelling is
  what a FROM → TO migration note is for.
- the `manifestId` values in the local install-ledger fixtures
  (packages/cli/src/commands/doctor-ledger-*.test.ts,
  packages/cloud-connection/src/*.test.ts) — that is a plain `manifestId: string`
  field on the ledger entry interface, judged by no schema; it is not
  `ManifestSchema.id` and not `PackageSchema.manifestId`.
- the `Not A Reverse Domain` fixtures in packages/spec — the negative controls of
  the template-manifest gate and its test.

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

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 866c8cfc904d6e59a9214f4e7457b591134b6989

Isolated at-tier reviewer, round 2 on this head; reviewed 2026-09-20T18:12Z. Merge-base 8271c814253 (unchanged from round 1); origin/main tip during the review 8ddefbc977 (17 commits past the merge-base, one of them — 2d892dd5cd — touching one PR path, packages/spec/src/stack-email-template-locale-floor.test.ts, with no id: line in its diff; GitHub reads the PR mergeable_state: clean); one detached worktree ../objectstack-review-18319-r2 at 866c8cfc90 (fresh pnpm install --frozen-lockfile, @objectstack/spec built, then the lint / create-objectstack / client-react and cli dependency closures), removed at the end. Under re-test: round 1's ⛔ F1 (record 5751237820, head 6783718d6a, reviewed 2026-09-20T16:56Z) — 「zero literal refused manifest.id values remain in the tree」. Round 1 is read as the prior verdict on a superseded head; every reading below was taken again here, and the dev's report 5751523635 was treated as a claim to falsify. Every zero has a lit control; exit codes were captured by redirect-then-$?, the suites through scripts/pm/os-verify-lock.sh with the verdict read from its VERDICT command-exit line.

① Derived judgments

  1. F1 closure — the tree-wide sweep, re-derived with a stronger instrument. Instrument: every tracked text file (9058 files, 173 MB) through seven extraction legs — A balanced-brace manifest: { … id } at depth 1 (any line distance, JSON "manifest": { included), B a 3-line window (cross-check), C manifestId / manifest_id, D a variable named *manifest* holding { id }, E an aliased body manifest: IDENT resolved to const IDENT = { id }, plus two the dev's five-leg sweep did not have: F id: IDENT inside a manifest body resolved to const IDENT = '…', and G a factory manifest: fn(…) resolved to the object literal fn returns — every literal judged by the BUILT ManifestSchema.shape.id from packages/spec/dist/kernel/index.mjs (self-check before scanning: accepts com.steedos.crm / org.apache.superset, refuses control_bad_id / blank / '', or the run throws; cross-check: the built PackageSchema.manifestId agrees on every probe). Lit control: an injected file carrying all five shapes (manifest: { id: 'control_bad_id' }, an aliased body, id: CTL_CONST, a factory return, manifestId: '…') — 5 of 5 reported refused on the right legs, the injected directory removed again, git status --porcelain empty. Real run: 626 unique id-sites, 608 literals, 42 refused, 1 template, 17 unresolved identifiers (16 are TypeScript id: string annotations in helper signatures, one a runtime value). The 42, each read in context: the declared residue — this PR's changeset FROM examples my_app / com.acme.my_app (2), the install-door pin packages/runtime/src/domain-handler-registry.test.ts:591 pkg-a (1, leg D only — a brace-key sweep cannot see it, as the dev said), the template-manifest negative controls packages/spec/scripts/check-template-manifests.ts:177 and packages/spec/src/marketplace/template-manifest-id.test.ts:63 Not A Reverse Domain (2, plus one regex artefact of my own leg C on check-template-manifests.ts:179, a startsWith('manifestId:') string, not an id); the ruled-to-stay refusal pins packages/spec/src/api/package-api.test.ts:914 ' ' and packages/cli/test/package-publish-manifest-id.test.ts:329 local.2024-app (2 — the intentional-negative class ruling 5689818081 keeps); the local install-ledger manifestId fixtures billing / broken / clean (13 literals + 1 interpolated across packages/cli/src/commands/doctor-ledger-*.test.ts and packages/cloud-connection/src/*.test.ts — a different key, ③); interpolations conforming by construction (packages/cli/src/commands/init.ts:627/721/799 com.example.${manifestIdSlug(name)}, seven cli e2e fixtures com.example.${ns} whose ns arguments are all bare lowercase alnum — amx, cfail, dsabsent … — measured at the call sites); and eight window-only false positives that are not manifest ids (c1 eval-case ids at three sites, an admin user id, p1 / x mock rows, nav_accounts, a v0 artifact's plugins[].id) — 2 + 1 + 2 + 1 + 2 + 13 + 1 + 10 + 8 = 40 accounted for. That leaves two hits outside every declared class, both invisible to a manifest: {-keyed sweep: (a) packages/metadata-protocol/src/protocol.package-publish-audit-rows.test.ts:372 const PKG = 'pkg_helpdesk', reaching manifest: { id: PKG, namespace: NS } at :1105 (a registry.getPackage stub; also the packageId of ~30 saveMetaItem / publishPackageDrafts calls and two toContain(PKG) assertions in the same file; never parsed by ManifestSchema, so no suite reds) — a fixture manifest id the pattern refuses, inside the population ruling 5689818081 renames in this PR, found by leg F. Measured mechanical: that file runs 1 file / 20 passed at head (command-exit 0), and with the single constant rewritten to com.example.helpdesk in my worktree it runs 1 file / 20 passed again (command-exit 0) — the two toContain(PKG) assertions read the constant, nothing reads the old spelling; the edit was restored byte-identical to HEAD afterwards (git hash-object = the HEAD: blob, porcelain empty). (b) .changeset/18490-nav-contribution-groups-imports-package-id-owner.md:11 manifest: { id: '', name: '' } — a FOREIGN pending changeset (on the merge-base, not this PR's; check-empty-changeset's finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 rule forbids this PR to edit it), taken to ③. The round-1 residue itself is closed: the 13 crm_core / crm_service ids, com.example.my_erp (in the spec tarball, files: prompts), my-app, acme-crm ×2, plugin-auth ×4, the README crm ×3 and the two checklist probe families — git grep for every old value reads 0 tree-wide outside this PR's own changeset, the new spellings 683 occurrences (control); the F1 commit 866c8cfc90 touches exactly those 10 files (56+/44−) and packages/create-objectstack/src/rewrite-identity.test.ts:31 (xcom.example.x, the dev's own extra find). Doc samples still compile: pnpm --filter @objectstack/spec check:docscommand-exit 0 (225 generated files in sync, import examples resolve against api-surface/); check:skill-examplescommand-exit 0 (258 prose examples type-check across 3 surfaces, 225 of them the skills + docs blocks — the gate that compiles the os:check fences the F1 commit edited; it is tsc-only, so the regex verdict on those three samples is my sweep's: com.acme.crm ×2 and com.example.my-app accept). The two platform-checklist items carry revision 1 → 2 and a history entry dated 2026-09-20 with ref: "#18319", as docs/qa/platform-checklist/README.md requires of a semantic edit; pnpm check:platform-checklist exit 0 (31 + 221 self-test assertions, 264 items). ⇒ The ruled contract population is closed for everything an author or a schema can reach, and the guidance residue is gone; but the PR-body claim 「zero literal manifest.id values the pattern refuses remain in the tree outside three declared residues」 is falsified at this head by one fixture literal in a shape the dev's instrument did not resolve. WRONG as stated — see ⛔ F1.
  2. One declaration, two references, byte-identical to round 1. git diff 6783718d6a 866c8cfc90 -- packages/spec/src/kernel/manifest.zod.ts packages/spec/src/marketplace/package.zod.ts packages/spec/src/kernel/manifest.test.ts is 0 bytes; the F1 commit's 10 paths include none of them. At head: MANIFEST_ID_PATTERN declared once at manifest.zod.ts:262, ManifestSchema.id .regex(MANIFEST_ID_PATTERN, …) at :351, PackageSchema.manifestId .regex(MANIFEST_ID_PATTERN, …) at package.zod.ts:174 via the import at :35. RIGHT.
  3. Refusal remedy arms, on the built dist. blankcom.example.blank; my_appcom.example.my-app; com.dogfood.flow_fixturecom.dogfood.flow-fixture; com.example.my_appcom.example.my-app; Com.Example.App, '', ' ', com., 2app → refused with no suggestion; every suggestion offered parses green; MANIFEST_ID_EXAMPLES = ['com.steedos.crm','org.apache.superset'], both match; the message names the key, echoes the value and shows both examples. Both schemas agree on all ten probes. RIGHT.
  4. Both scaffolders. No scaffolder source moved since round 1 (the F1 commit touches only a create-objectstack test fixture). Re-run here: pnpm --filter create-objectstack test 16 files / 215 passed, command-exit 0; pnpm --filter create-objectstack typecheck command-exit 0; the os init pin packages/cli/test/init-manifest-id.test.ts (my-app → namespace my_app → id com.example.my-app, through the imported pattern and ManifestSchema.safeParse) 1 file / 13 passed, command-exit 0 (run on the unit project after building the cli closure); the bundled template still ships com.example.blank. RIGHT.
  5. ADR-0087 entry. packages/spec/src/migrations/entries/semantic/18.manifest-id-reverse-domain-required.ts present; registry.ts:9666 carries the generated row; the changeset carries the marker; check-adr-0087-registration --self-test 0 and --base origin/main 0 ([BREAKING+bang] registered manifest-id-reverse-domain-required, new here). RIGHT.
  6. ADR-0130 D4 block vs the code. The block (+38/−0 after the reservation paragraph) is byte-identical to round 1's. Code at head: DOOR 1 = ArtifactPackageSchema.safeParse(entry) at packages/core/src/artifact-packages.ts:224refuse('INVALID_ARTIFACT_PACKAGE_ENTRY', …) at :231 (status: 422, :152); DOOR 2 = artifactPackageId (:172, still id || name) consulted only at :251, after DOOR 1 threw; the pin block #13457 / #17534 — which unattributable-consent spellings the doors refuse, and at which door at packages/runtime/src/security/artifact-granted-permissions.test.ts:212 with its DOOR 1 cases at :224 / :237; packages/core/src/security/granted-permissions-not-enforced.pin.test.ts exists as the block says. check:adr-anchors 0, check:adr-symbol-anchors 0 (2113 anchors across 140 records resolve), check:adr-links 0 (688 destinations). An addition to D4, not a reversal. RIGHT.
  7. The two merge-forced pins. Neither file is in the F1 commit, so both are byte-identical to round 1's reading. packages/spec/src/api/package-api.test.ts ran inside the spec suite here (503 files / 14735 passed, command-exit 0); packages/cli/src/utils/nav-contribution-groups.package-id.test.ts is integration tier and was NOT MEASURED locally this round — CI Test Core 1–6 green on this head carries it. RIGHT / cli leg NOT MEASURED locally.
  8. Merge faithfulness. git diff --name-only origin/main...866c8cfc90 = 104 paths = the PR's file list exactly (diff empty); 1068+ / 249−. pnpm --filter @objectstack/spec check:generatedcommand-exit 0, 「All 15 generated artifacts are up to date」, tree clean afterwards; the F1 commit's packages/spec/prompts/create-new-project.md edit owes no regeneration (check:llms-txt / check:published-files are among the 138 derived families the dev reports green, not re-run here). RIGHT.
  9. packages/services/service-package/README.md and the changeset question, measured. The file ships: @objectstack/service-package 17.4.0 is public with files: ["dist","README.md","CHANGELOG.md"]. No script named check-changeset-presence exists in scripts/; the dispatch-gates.mjs --commands derivation over the 104 paths (138 families) names check-changeset-fixed, check-changeset-no-major (+ self-test), check-empty-changeset (+ self-test), check:changeset-gate-self-tests, check:objectui-changeset and check:pm-changeset-deadline-census; the presence check itself is the Check Changeset job in .github/workflows/pr-automation.yml, whose reading is the count of .changeset/*.md files this PR ADDS (git diff --diff-filter=A from the merge base) — presence-only, with no per-package axis, and none of the three scripts adds one (check-empty-changeset judges frontmatter emptiness and foreign edits; check-changeset-fixed judges the fixed group's membership). This PR adds one; the job is green on the head (2026-09-20T17:50Z) and the four scripts exit 0 here. Semantically: .changeset/config.json has ONE fixed group of all 70 public packages, @objectstack/service-package and @objectstack/spec both in it, so the README rename ships in the same release as the spec bump whatever the changeset names; and AGENTS.md's step 3 owes a changeset for a feature, functional improvement or fix — an example-id rename in a README is none. No changeset is owed by the gate's own reading, nor by the fixed-group release mechanics. RIGHT.
  10. Governed reading. check-governed-merges.mjs --branch HEAD → exit 3, GOVERNED, tier H, docs/adr/** ×1 of 104, 1317 changed lines ≤ 5000; the PR is draft, auto_merge null, labels carry needs:contract-review. RIGHT.

Suites and gates run here, each through os-verify-lock.sh: pnpm --filter @objectstack/lint test 106 files / 4005 passed, command-exit 0; pnpm --filter @objectstack/lint typecheck command-exit 0 (its check:test-typecheck leg compiles the renamed test file); pnpm --filter @objectstack/spec test 503 files / 14735 passed, command-exit 0; pnpm --filter @objectstack/spec typecheck command-exit 0 (its check:test-typecheck leg compiles the test layer); pnpm --filter create-objectstack test 16 files / 215 passed, command-exit 0; pnpm lint (repo-wide) command-exit 0 (eslint . --no-inline-config, the full population); check-changeset-no-major --base origin/main 0 (self-test 0), check-adr-0087-registration --base origin/main 0 (self-test 0), check-empty-changeset --base origin/main 0 (self-test 0; 1 declaring changeset added, no merge-base changeset modified), check-changeset-fixed 0 (70 public packages in sync), check:platform-checklist 0, the three ADR gates 0. NOT MEASURED here: the packages/qa dogfood tier, the cli integration shards, the remaining dispatch-gates families beyond those named (the dev reports 138/138 run — that reading is the dev's, not re-run in full here; CI's required contexts carry them), and check:react-declaration-parity (needs the objectui manifest input). CI on the head at review time (2026-09-20T18:10Z): 50 check runs, 42 success / 8 skipped / 0 failure / 0 in progress; all seven required contexts successLint & Repo Gates (completed 2026-09-20T17:40Z), TypeScript Type Check, Test Core (1–6), Dogfood Regression Gate (1–3), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; Check Changeset success; GitHub reads the PR mergeable_state: clean, draft, auto_merge null.

② Semver level

.changeset/17534-manifest-id-reverse-domain.md is not in the F1 commit and is byte-identical to round 1's: @objectstack/spec, @objectstack/cli, create-objectstack at minor; the BREAKING banner (accept-set sense, launch-window minor rationale); FROM → TO with the one-line fix; the adr-0087: registered manifest-id-reverse-domain-required marker; the identity/republish warning; and the 「fail-OPEN → fail-CLOSED」 section naming the direction, the population and the provenance. Gate exit codes on this head, self-tests first: check-changeset-no-major --base origin/main 0 (no major; its clause-② axis is PR-payload-only and green as Check Changeset on CI), check-adr-0087-registration --base origin/main 0, check-empty-changeset 0, check-changeset-fixed 0. Clause-②: yes on both carriers (check-clause2-carriers --pair 18319 → exit 0 at review time, governing claim 5681542663, PR body declaring yes); three new public symbols on @objectstack/spec/kernel make minor the floor. RIGHT.

③ Boundary flags

Implemented-by: claude/issue-17534-manifest-id-reverse-domain-regex
Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE

VERDICT: FAIL

F1 (residual) — ①1: the PR-body claim 「zero literal manifest.id values the pattern refuses remain in the tree outside three declared residues」 is falsified at 866c8cfc90 by packages/metadata-protocol/src/protocol.package-publish-audit-rows.test.ts:372 const PKG = 'pkg_helpdesk', carried into manifest: { id: PKG, namespace: NS } at :1105 — a fixture manifest id inside the population ruling 5689818081 renames in this PR, in the one shape (id: IDENT resolved to a constant) the dev's five-leg instrument did not resolve. The remedy is one literal ('pkg_helpdesk' → a reverse-domain spelling, com.example.helpdesk measured green at 20/20 in ①1) on a non-governed path, plus the PR-body sentence rewritten to the population that then holds — or the maintainer strikes that one fixture explicitly; a claim the tree contradicts cannot stand as the review record, however small the margin. Everything else in ①–③ reads RIGHT on this head: the round-1 residue (13 lint fixtures, the spec prompt, five docs pages, the README, two checklist items) is closed and measured closed, and the ruled contract — pattern, refusal, examples, scaffolders, ADR-0087 entry, ADR-0130 D4 block, the two pins, merge faithfulness, changeset — is delivered unchanged since round 1.


Generated by Claude Code

…to reverse-domain

`protocol.package-publish-audit-rows.test.ts` declared `const PKG =
'pkg_helpdesk'`, which reaches `manifest: { id: PKG, namespace: NS }` on the
`registry.getPackage` stub and is refused by `MANIFEST_ID_PATTERN`. The value
becomes `com.example.helpdesk`.

Every use reads the constant -- the three `toContain(PKG)` assertions included
-- so each case keeps its meaning; the sibling assertion on the namespace
prefix (`helpdesk_`) is unaffected, since the new value does not contain it.

Verified against the BUILT `ManifestSchema.shape.id` after a spec build, with a
lit control on the judge: 'pkg_helpdesk' refused, 'com.example.helpdesk'
accepted.

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

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1d4c99bcbdf082c937d06157d488d8ac191939b6

Isolated at-tier reviewer, round 3 on this head; reviewed 2026-09-20T19:07Z. Merge-base 8271c814253 (unchanged from rounds 1–2); origin/main tip during the review f20fe298a1; one detached worktree ../objectstack-review-18319-r3 at 1d4c99bcbd (fresh pnpm install --frozen-lockfile, @objectstack/spec built, then the @objectstack/metadata-protocol dependency closure), removed at the end; the diff vs the round-2 head 866c8cfc90 proven to be exactly one file / one line (①1 below), so every item the diff does not reach cites round 2's record 5751666135 by id only after (1) and (4) proved the tree otherwise identical. Under re-test: round 2's ⛔ F1 (residual) — const PKG = 'pkg_helpdesk'. The dev's report 5751864726 on #17534 was treated as a claim to falsify; every zero below has a lit control; exit codes were captured by redirect-then-$?, every build and suite through scripts/pm/os-verify-lock.sh (slot review-18319-r3, lock free on every acquisition) with the verdict read from its VERDICT command-exit line.

① Derived judgments

  1. (1) The diff vs round 2 is exactly one line. git diff 866c8cfc90 1d4c99bcbd --stat = 1 file, +1 / −1: packages/metadata-protocol/src/protocol.package-publish-audit-rows.test.ts:372 'pkg_helpdesk''com.example.helpdesk'; git log 866c8cfc90..1d4c99bcbd = one commit (1d4c99bcbd); no other path, no rebase, no amend (the round-2 head is an ancestor). RIGHT.
  2. (2) The renamed constant under the BUILT judge. pnpm --filter @objectstack/spec build command-exit 0 (114s). Judge self-test before any reading, or the instrument throws: ManifestSchema.shape.id from packages/spec/dist/kernel/index.mjs accepts com.steedos.crm / org.apache.superset and refuses control_bad_id / blank / '', and the built PackageSchema.manifestId agrees on every probe. pkg_helpdesk REFUSED on both schemas (the message suggests com.example.pkg-helpdesk, which parses); com.example.helpdesk ACCEPTED on both. git grep -n pkg_helpdesk at 1d4c99bcbd = 0 tree-wide, with two lit controls: the same grep at 866c8cfc90 = 1 (:372), and com.example.helpdesk at the head = 1 (:372). All 30 PKG references read the constant; the three toContain(PKG) assertions (:488, :662, :1163) and the registry stub manifest: { id: PKG, namespace: NS } at :1105 are untouched; the file imports ManifestSchema nowhere (0 occurrences), so its 20/20 green is a no-regression reading, exactly as the dev's report says — the evidence for the rename is the sweep in ①3 plus the rename population ruling 5689818081. RIGHT.
  3. (3) The round-2-class sweep, rewritten and re-run here at the head. Instrument: every tracked or untracked-unignored text file (9070 listed, 9063 scanned, 7 binaries skipped, 188 MB) through eight legs — A balanced-brace manifest: {…} at depth 1 at any line distance (JSON "manifest": { included), B a 3-line window (cross-check; YAML/MD bare values judged), C manifestId / manifest_id (literal or same-file constant), D a variable whose name contains manifest holding { id }, E an aliased body manifest: IDENT resolved to const IDENT = {…}, F id: IDENT inside a manifest body resolved to a same-file string constant, G a factory manifest: fn(…) resolved to the object literal it returns (arrow and function forms), and H the inverse: every same-file string constant the pattern refuses that reaches an id: / manifestId: / manifest_id: / packageId: position — every value judged by the BUILT ManifestSchema.shape.id, cross-checked against the built PackageSchema.manifestId (0 disagreements over 1439 judged). Lit control, twice: a 14-file corpus carrying 12 refused shapes (literal, JSON literal, aliased body, id: CONST, arrow factory, function factory, manifestId literal, manifestId via constant, const manifest = {…}, YAML, an id key beyond any 3-line window, and the exact const PKG / registry-stub manifest: { id: PKG, namespace: NS } shape of the round-2 F1) plus 2 accepted controls — 12 of 12 reported on the right legs (the first build missed the JSON-key and YAML shapes; both fixed before any real reading was believed) and the 2 accepted seen and not flagged; then the same corpus injected UNTRACKED under the worktree, the real enumeration reporting all 12 (20 rows) with the non-control rows byte-identical to the clean run, removed again, git status --porcelain empty. Real run: 1624 unique id-sites, 1439 judged, 315 refused, of which 250 are leg-H rows with no manifest in the six preceding lines and a plain id: / packageId: key (e.g. app-crm's PACKAGE_ID = 'app:com.example.crm' reaching packageId: — not this key), and 0 leg-H rows reach manifestId: / manifest_id: or carry a manifest in scope — the dev's instrument-2 zeros, reproduced. The 48 remaining unique refused sites, each read in context, are exactly the declared residue: this PR's changeset FROM examples my_app / com.acme.my_app (2); the foreign pending changeset .changeset/18490-nav-contribution-groups-imports-package-id-owner.md:11 empty id (1, ③); the local install-ledger manifestId fixtures billing / broken / clean — 13 literals + 1 ${manifestId} interpolation across packages/cli/src/commands/doctor-ledger-*.test.ts and packages/cloud-connection/src/*.test.ts, a plain interface field carrying neither key that holds the pattern (14); the spec template-manifest negative controls packages/spec/scripts/check-template-manifests.ts:177 and packages/spec/src/marketplace/template-manifest-id.test.ts:63 Not A Reverse Domain (2, plus one leg-C artefact at :179, a startsWith('manifestId:') string, not an id); the ruled-to-stay refusal pins packages/spec/src/api/package-api.test.ts:914 ' ' and packages/cli/test/package-publish-manifest-id.test.ts:329 local.2024-app (2), and — same intentional-negative class, this PR's own DOOR 1 pins — packages/runtime/src/security/artifact-granted-permissions.test.ts:262 / :303 id: '' inside tests titled 「is refused at DOOR 1」 (2; window-only, not individually named in the dispatch, stated here so nobody rediscovers them); the install-door pin packages/runtime/src/domain-handler-registry.test.ts:591 pkg-a (1; legs A, B and D); template interpolations conforming by construction, re-measured at their call sites rather than taken from round 2 — packages/cli/src/commands/init.ts:627/721/799 com.example.${manifestIdSlug(name)} and com.objectstack.plugin-${manifestIdSlug(name)}, where manifestIdSlug (:95-103) lowercases, folds every non-alphanumeric run to -, trims hyphens and forces a leading letter; seven cli e2e fixtures com.example.${ns} whose ns arguments are all bare lowercase alpha (sbody, zfail, znc, rfail, cfail, amx, dfail, lfail, capfail, tfail, cvt, cnl, wrn, sug, err, cln, pfail, pnc, ctrl, dsabsent, dsempty, dstwo); and packages/runtime/src/packages-unscoped-environment-binding.test.ts:107 pkg.of.${label} whose only makeKernel label is host-default (11); and twelve window-only rows that are not manifest ids at all (c1 eval-case ids ×3 beside a com.example.a manifest, npx create-objectstack a scaffolder label, admin a session user id, p1 / x mock rows, group_service ×2 a navigation[].id beside com.example.crm-core, legacy_u1 a seed record id beside com.test.nested-seed, nav_accounts, a v0 artifact's plugins[].id) — 2 + 1 + 14 + 3 + 4 + 1 + 11 + 12 = 48. Stated limit: legs C/E/F/G resolve same-file only; the two value-bearing cross-file aliases the sweep left unresolved (manifest: authPluginManifestHeader / securityPluginManifestHeader in the two plugin objectstack.config.ts) were resolved by hand to AUTH_PLUGIN_ID = 'com.objectstack.plugin-auth' and SECURITY_PLUGIN_ID = 'com.objectstack.plugin-security', both accepted; the other 52 unresolved names are type aliases, schema references, function parameters, or runtime values (request.targetPackageId at protocol.ts:19259 among them, ③). Refused set = declared residue; nothing outside it. RIGHT.
  4. (4) The three MANIFEST_ID_PATTERN sites. git diff 866c8cfc90 1d4c99bcbd -- packages/spec/src/kernel/manifest.zod.ts packages/spec/src/marketplace/package.zod.ts packages/spec/src/kernel/manifest.test.ts = 0 bytes. At head: declared once at manifest.zod.ts:262; ManifestSchema.id .regex(MANIFEST_ID_PATTERN, …) at :351; PackageSchema.manifestId .regex(MANIFEST_ID_PATTERN, …) at package.zod.ts:174 via the import at :35. RIGHT.
  5. (5) Merge faithfulness. git diff origin/main...1d4c99bcbd --name-only = 105 paths; GET /pulls/18319/files (2 pages, 100 + 5) = 105; diff of the two sorted lists empty; +1069 / −250 on both readings (the API and git diff --numstat). Merge-base 8271c814253, unchanged. RIGHT.
  6. (6) CI on the head, as a reading. At my first reading Lint & Repo Gates was in_progress; at the final re-read (2026-09-20T19:06Z) all seven required contexts read completed / success: TypeScript Type Check, Test Core (aggregate and 1–6), Dogfood Regression Gate (aggregate and 1–3), Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates (completed 2026-09-20T18:54Z), Governed Surface Queue Guard; 43 check runs, 37 success / 6 skipped / 0 failure / 0 in progress; Check Changeset success. GitHub reads the PR draft, mergeable: true, mergeable_state: blocked at my first reading (while Lint & Repo Gates ran) and clean at the final re-read, auto_merge null, labels carrying needs:contract-review. RIGHT.
  7. Refusal remedy arms, on the built dist (re-probed here because it is cheap). blankcom.example.blank; my_appcom.example.my-app; com.dogfood.flow_fixturecom.dogfood.flow-fixture; com.example.my_appcom.example.my-app; pkg_helpdeskcom.example.pkg-helpdesk; Com.Example.App, '', ' ', com., 2app refused with no suggestion; every suggestion offered parses green; every message names its key (manifest.id on one schema, manifestId on the other), echoes the value and shows both MANIFEST_ID_EXAMPLES (com.steedos.crm, org.apache.superset, both matching the pattern). RIGHT.
  8. Both scaffolders, the ADR-0087 entry, the ADR-0130 D4 block vs the code's door order, and the two merge-forced pins — unchanged since 5751666135, tree proven identical by (1)/(4): none of their files is in the one-line diff. Re-run here where cheap: check-adr-0087-registration --self-test 0 (441 assertions) and --base origin/main 0 ([BREAKING+bang] registered manifest-id-reverse-domain-required, new here); packages/spec/src/api/package-api.test.ts ran inside the spec suite below; the cli integration leg packages/cli/src/utils/nav-contribution-groups.package-id.test.ts stays NOT MEASURED locally (CI Test Core 1–6 green on this head carries it), as in round 2. RIGHT / cli leg NOT MEASURED locally.
  9. Governed reading. check-governed-merges.mjs --branch HEAD → exit 3, GOVERNED, tier H, docs/adr/** ×1 of 105 (docs/adr/0130-release-artifact-as-co-ownership-boundary.md), 1319 changed lines ≤ 5000; the PR is draft, auto_merge null. The landing stays the maintainer's hand. RIGHT.

Suites and gates run here, each through os-verify-lock.sh: the changed file alone (pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.package-publish-audit-rows.test.ts) 1 file / 20 passed, command-exit 0; pnpm --filter '@objectstack/metadata-protocol^...' build command-exit 0 (158s); pnpm --filter @objectstack/metadata-protocol test 182 files passed + 3 skipped (185), 2596 tests passed + 19 skipped (2615), command-exit 0; pnpm --filter @objectstack/spec test 503 files / 14735 passed, command-exit 0 (127s); pnpm lint (node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config, the full population, not narrowed) command-exit 0 (73s); check-changeset-no-major --self-test 0 (365 assertions) and --base origin/main 0; check-empty-changeset --self-test 0 and --base origin/main 0 (1 declaring changeset added, no merge-base changeset modified); check-changeset-fixed 0 (70 public packages in sync); check-clause2-carriers --pair 18319 exit 0 (the declaration read from the correction comment on the card as yes; the needs:contract-review label in the same state on both carriers). NOT MEASURED here: the packages/qa dogfood tier, the cli integration shards, the remaining dispatch-gates families (the dev reports 51 of 138 run for this commit's own attribution and names the 87 belonging to paths the commit does not touch — that reading is the dev's; CI's required contexts carry them), and check:react-declaration-parity (needs the objectui manifest input).

② Semver level

git diff 866c8cfc90 1d4c99bcbd -- .changeset is empty, so .changeset/17534-manifest-id-reverse-domain.md is byte-identical to the round-2 head — @objectstack/spec, @objectstack/cli, create-objectstack at minor; the BREAKING banner (accept-set sense, launch-window minor rationale); FROM → TO with the one-line fix; the adr-0087: registered manifest-id-reverse-domain-required marker; the identity/republish warning; the 「fail-OPEN → fail-CLOSED」 section naming direction, population and provenance — unchanged since 5751666135, tree proven identical by (1)/(4). Gates re-run on this head, self-tests first: check-changeset-no-major --base origin/main 0 (its clause-② axis is PR-payload-only and green as Check Changeset on CI), check-adr-0087-registration --base origin/main 0, check-empty-changeset 0, check-changeset-fixed 0. Clause-②: yes on both carriers (--pair 18319 exit 0). Three new public symbols on @objectstack/spec/kernel make minor the floor. minor, carrying the BREAKING banner and the ADR-0087 semantic entry. RIGHT.

③ Boundary flags

  • Duplicate-package door — CONFIRMED by reading; not this PR's blocker; carried. packages/metadata-protocol/src/protocol.ts:19257-19264 builds dupManifest = { ...srcPkg.manifest, id: request.targetPackageId, name: …, namespace: targetNs } and hands it to this.installPackage({ manifest: dupManifest }) inside a swallowing try/catch; grep ManifestSchema protocol.ts returns two comment lines (:7865, :7869) and no parse; :19123 defaults targetNs to request.targetPackageId.split('.').pop() — the door relies on the reverse-domain shape it does not enforce. Carried as the second seam on [finding] the HTTP install door reads manifest.id positionally and never parses the body through ManifestSchema — POST /packages answers 201 to ids that MANIFEST_ID_PATTERN (spec, defineStack, os build, the publish face) refuses #19417 (comment 5751874616), the same class as that card's install-door seam packages/runtime/src/domains/packages.ts:755-760, which is unchanged at this head (git diff 8271c81425 HEAD -- packages/runtime/src/domains/packages.ts empty). I file nothing.
  • Foreign changeset 18490 — CONFIRMED false at this head; carried. .changeset/18490-nav-contribution-groups-imports-package-id-owner.md:11 states as its measured premise that { manifest: { id: '', name: '', … } } parses green; at this head '' is refused on manifest.id by both built schemas (①7) and at DOOR 1 (artifact-granted-permissions.test.ts:262). This PR may not edit it (finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 rule; check-empty-changeset green on exactly that basis); the written statement the changeset gate asks for is the seat's 5751673952 on [finding] ManifestSchema.id is a bare z.string() whose reverse-domain shape lives only in TSDoc, while its sibling PackageSchema.manifestId enforces that shape with a regex — one identifier, two declarations, only one of them machine-readable #17534, and the correction rides a docs-only follow-up under the maintainer's confirmation — a seat act, not a blocker.
  • PR-body sentence — a seat body edit owed before the maintainer's word is requested; not a tree finding. The body still reads 「at head 866c8cfc90 … zero literal manifest.id values the pattern refuses remain in the tree outside three declared residues … measured by a five-leg sweep」. The substance holds at 1d4c99bcbd (①3), but the head the sentence pins the claim to is the one round 2 falsified, and the instrument it cites is the one that could not resolve a constant. The dev made no body PATCH (outside its write budget, as its report says) and this seat is barred from body writes, so the edit is the director seat's: re-pin the sentence to 1d4c99bcbd and to an instrument that resolves constants (the dev's two-instrument report 5751864726, or ①3 here). Round 2's F1 named this half of its remedy; it is stated here so it is not lost. It does not block: the review record for this head is this comment, and the landing is Tier H.
  • Local install-ledger manifestId: string — unchanged: packages/cloud-connection/src/local-manifest-source.ts:40 is a plain interface field outside both keys that carry the pattern; a separate contract question, not this PR's, and the sweep excludes it on that basis.
  • needs:contract-review — the dev's report noted it absent at push time; it was present on the PR at every reading of this round, and check-clause2-carriers --pair 18319 reads the two label carriers in the same state.

Implemented-by: claude/issue-17534-manifest-id-reverse-domain-regex
Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE

VERDICT: PASS

Round 2's ⛔ F1 (residual) is closed at 1d4c99bcbd: the one constant is renamed to a spelling the built judge accepts, the old spelling reads 0 tree-wide with lit controls, and the reviewer's own eight-leg sweep with two injected controls finds no refused manifest.id outside the declared residue. Everything else in ①–③ is delivered unchanged from the round-2 head, proven identical by the one-line diff and the 0-byte diff on the three pattern files, and the suites and gates named by the dispatch read command-exit 0 here.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Provenance — carriers cleared on PASS, director seat (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-20T19:08Z.

  • Contract review of record: 5751970203 (round 3), Served-tier: CONTRACT_REVIEW_TIER — the reviewer's transcript re-read by this seat: every request at tier, zero fallbacks (round 1: 224/224, round 2: 243/243, round 3 likewise); judged head 1d4c99bcbdf082c937d06157d488d8ac191939b6, the current head. Independence pair Implemented-by: claude/issue-17534-manifest-id-reverse-domain-regex / Reviewed-by: session_012GcsUbuqFGBibkEDMRC1eE (the dev rounds were mode:subagent under this seat; the reviewers were isolated subagents whose verdicts this seat adopted verbatim).
  • Rounds 1 and 2 (5751237820, 5751666135) FAILed on the fixture-rename residue and were each closed by one fix round; round 3 proved the tree identical to round 2 except the one-line rename and re-swept it (declared residue only).
  • needs:contract-review stripped from this PR and from [finding] ManifestSchema.id is a bare z.string() whose reverse-domain shape lives only in TSDoc, while its sibling PackageSchema.manifestId enforces that shape with a regex — one identifier, two declarations, only one of them machine-readable #17534 in this stroke; check-clause2-carriers --pair 18319 exit 0 (reviewer's run, 5751970203). CI at the review's final read: all seven required contexts success, 0 red.
  • Landing: this is a governed diff (docs/adr/0130-*.md, tier H, 1319 lines, under the 5000 line). Ready for review now; review requested from GOVERNED_APPROVERS (os-zhuang, hotlong). ⛔ No seat arms auto-merge or enqueues a governed PR without an authorized APPROVED review; the maintainer's approval (or their own merge) lands it. Under batch 🔗 Broken links detected in documentation #200 item 3 (5750928080), maintainer 「同意」.

Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Enqueued by the director seat (session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-20T23:08Z: authorized APPROVED review on the current head — os-zhuang (a GOVERNED_APPROVERS account) at 2026-09-20T23:07Z on 1d4c99bcbd; the maintainer said so in chat (「18319 我已经批准了」). Seven required contexts success, mergeable_state: clean, contract review PASS 5751970203, size 1319 lines under the human-merge line. Auto-merge (squash) armed; the queue's governed leg reads the approval and lands it. Card #17534 closes with the landing record after the squash is on origin/main.


Generated by Claude Code

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 size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants