Conversation
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
…r vitest Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
📓 Docs Drift CheckThis PR changes 6 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 26edca599cd8fbf7aef75834ab340d784c31f4db && git checkout 26edca599cd8fbf7aef75834ab340d784c31f4db
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 37af6533be1af36671e52eefebfd7740e24d56f1 4f277510f35680bf85738999f891e03cbf6a5e8a && git checkout -B drift-repro 37af6533be1af36671e52eefebfd7740e24d56f1 && git merge --no-ff 4f277510f35680bf85738999f891e03cbf6a5e8a
node scripts/docs-audit/affected-docs.mjs --json 37af6533be1af36671e52eefebfd7740e24d56f1
|
This PR is red on purpose, and it is blocked on one maintainer decision — not on an unhandled defectWhat is failing, read from Why. The card's ruling adopts The reachability question that decides whether those 21 matter was answered by a failing probe with a lit control, before the change: So 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. 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:
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 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 <[email protected]>
`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 <[email protected]>
Status refresh — the blocker changed, and my earlier comment on this PR is now the wrong description of itMy comment Measured at head The fact that decides how serious this is: of the 63 from the first pass, 62 are in a Current CI, head 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 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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
…nifest-id-reverse-domain-regex
…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 <[email protected]>
…geset names the fail-OPEN reversal `ManifestSchema.id` now carries `MANIFEST_ID_PATTERN`, and `AssembledPackageBodySchema` extends `ManifestSchema`, so the artifact package entry schema carries it too. Three pins in the `#13457` door block described a world where `''` survived that schema: - `''` was refused by `artifactPackageId` one door later (DOOR 2); - `{ id: '', name: 'x' }` was refused by NEITHER door and carried as `x` through the `id || name` fallback; - a consent record keyed by `''` bound to nothing and the package still loaded with no consent record — fail-OPEN. All three are refused at DOOR 1 now. The pins are rewritten to the measured behaviour rather than loosened: each still names ITS OWN door by message and asserts the absence of the other door's, so none of them would survive deleting a whole door — the property the block's header exists to protect. The last of the three reverses direction, so the changeset says so in those words: fail-OPEN to fail-CLOSED on a consent/permission path, and the affected population is artifacts carrying `manifest.id: ''`, which the registry face has always refused at publish and whose granted-permissions consent already silently did not apply. The refusal is the artifact PACKAGE door refusing a malformed id, ⛔ not the permission seam acquiring teeth: #17147's repo-wide "registered, not enforced" pin is untouched and still green. #17148 (whether an unbindable consent record should refuse the artifact) is NOT settled by this and is noted as still open in the case that used to imply it. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <[email protected]>
…constants are exported (objectstack-ai#18462) Part of objectstack-ai#18169. Docs-only: one false sentence in published CHANGELOG text, amended in place. No source file is touched. ## The AGENTS.md rule this PR obeys Quoted from `AGENTS.md`, *Documentation Guardrails*, the `packages/*/CHANGELOG.md` row (read 2026-09-16T12:22Z at `e4449b4ef`): > `packages/*/CHANGELOG.md` | **RELEASE-OWNED** | ❌ Never edit in a code PR — `changeset version` compiles it from `.changeset/*.md` and it ships inside the npm tarball as the text an upgrading agent greps (§ *Post-Task Checklist* step 3). Your PR's input is its **changeset**, on a hard, unwatched deadline: the release that consumes it deletes that input and publishes the sentence. Factual error in a released entry → **amend that entry in a dedicated docs-only PR**, ⛔ never an erratum in a later entry and never a rider on code changes — the reader greps the tombstoned symbol and lands on the old entry, so a correction anywhere else is one it never reaches; what shipped stays recorded in the published tarballs and in git history. That is why the amendment is in place, at the entry the reader's grep lands on, and why this PR carries no source change. ## Old sentence, new sentence The entry is `## 17.3.0` → `- 1af8286:` (the objectstack-ai#13935 fix), `packages/lint/CHANGELOG.md:2176`. Line number derived here, not taken from the card. **Before:** ``` `FIELD_RULE_AMBIENT_ROOTS` and `FIELD_RULE_JUDGED_ROOTS` are exported beside the existing `FIELD_RULE_BOUND_ROOTS`. ``` **After:** a paragraph that states only what the export statement actually names — see the diff. Both symbol names are kept in the text on purpose: the rule's stated rationale is that the reader greps the symbol and lands on this entry, so removing the names would move the landing spot. ## The reading behind the new sentence Every number below was taken in this worktree, by this seat. ### Leg 1 — the export statement (the source of truth) `packages/lint/src/index.ts:53`, the package's only export site for this family: ``` export { validateStackExpressions, fieldRuleRootIssue, FIELD_RULE_BOUND_ROOTS } from './validate-expressions.js'; ``` Occurrences in `packages/lint/src/index.ts`, read **2026-09-16T12:41:03Z** at branch tip `8f8a47ce1` (base `e4449b4ef` = `origin/main`): | name | occurrences | note | |:--|--:|:--| | `FIELD_RULE_BOUND_ROOTS` — **lit control** | **1** | inside the export statement above | | `FIELD_RULE_NOWHERE_BOUND_ROOTS` — **dark control** | **0** | a real `export const` of `validate-expressions.ts` (`:734`), the very module line 53 imports from — so a 0 here reads "not re-exported", not "grep is broken" | | `FIELD_RULE_AMBIENT_ROOTS` | **0** | | | `FIELD_RULE_JUDGED_ROOTS` | **0** | exported from `validate-expressions.ts:746`, never re-exported from the entry | | `export *` (a second path to a consumer) | **0** | | The package declares a second published subpath, `./runtime`. Probed too, same reading time: `packages/lint/src/runtime.ts` contains **0** occurrences of `FIELD_RULE` and **0** star-exports. `exports` names exactly `.` and `./runtime` and no wildcard, so those two are the whole consumer-reachable surface. ### Leg 2 — the freshly built `dist` (this branch) Built here with `pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' build` (exit 0), then read **2026-09-16T12:40:43Z** at `8f8a47ce1`: | name | `dist/index.d.ts` | `dist/index.js` | in the export clause / export block? | |:--|--:|--:|:--| | `FIELD_RULE_BOUND_ROOTS` (lit control) | 3 | 3 | **yes**, both | | `FIELD_RULE_AMBIENT_ROOTS` | 0 | 0 | no | | `FIELD_RULE_JUDGED_ROOTS` | 2 | 2 | **no** — both `.d.ts` hits are `{@link}` docblock references | | `FIELD_RULE_NOWHERE_BOUND_ROOTS` | 1 | 4 | no |⚠️ Note the shape: a raw occurrence count on a bundled `.d.ts` is **not** the right probe. `FIELD_RULE_JUDGED_ROOTS` is present in the emitted declaration file and still not exported. The probe that decides is membership of the export clause, which is what the table's last column reads. ### Leg 3 — the published tarball (what a consumer actually installs) `npm view @objectstack/lint version` → `17.4.0`; `https://registry.npmjs.org/@objectstack/lint/-/lint-17.4.0.tgz` downloaded and unpacked, read **2026-09-16T12:25:43Z**: - `package/CHANGELOG.md:2176` carries the false sentence verbatim. The defect is published, not merely in-tree. - `package/dist/index.js`: the ESM export block begins at line 14522; the only member of this family inside it is `FIELD_RULE_BOUND_ROOTS` (line 14585). Every `FIELD_RULE_AMBIENT_ROOTS` / `FIELD_RULE_JUDGED_ROOTS` occurrence sits at lines 1163–1253, i.e. in the bundle body, above the export block. - `package/dist/index.d.ts`: same — the export clause names `FIELD_RULE_BOUND_ROOTS` and neither of the other two. ⇒ `import { FIELD_RULE_AMBIENT_ROOTS } from '@objectstack/lint'` fails to resolve against the currently published package. Acted on, the sentence fails. ## `files[]` readings, and the changeset argument Packages the diff touches, read 2026-09-16T12:41:03Z at `8f8a47ce1`: | package | path in this diff | `private` | `files[]` | does this diff move published bytes? | |:--|:--|:--|:--|:--| | `@objectstack/lint` 17.4.0 | `packages/lint/CHANGELOG.md` | `false` | `["dist","README.md","CHANGELOG.md"]` | **yes** — `CHANGELOG.md` is whitelisted, and the published 17.4.0 tarball above contains it | | `@objectstack/spec-monorepo` 4.0.1 (repo root) | `.changeset/lint-changelog-export-claim.md` | `true` | none | no — private, and `.changeset/` is release input, not shipped | **So `skip-changeset` is unavailable by its own definition.** AGENTS.md, *Post-Task Checklist* step 3: "A bug fix in a released package takes a **`patch`** changeset — never none, and ⛔ never `skip-changeset`: that label is for a diff that publishes nothing from any released package." `@objectstack/lint` is a released package (17.4.0 is on the registry, read above) and this diff moves bytes inside its tarball. The usual docs-only reflex does not reach this file. ⇒ **a `patch` changeset is added.** **The circularity, stated rather than ignored.** A changeset's own release appends a new entry to the top of the very file being corrected. Three readings settle it: 1. **The amendment survives that release.** `changeset version` *prepends* under the package heading — read off this file's own structure: `## 17.4.0` at line 3, `## 17.3.0` at 781, `## 17.2.0` at 3075, descending. Older entries are not rewritten, so the corrected 17.3.0 paragraph is durable. 2. **The new entry is not the correction, and is not an erratum in the sense the rule forbids.** AGENTS.md forbids correcting *by* an erratum in a later entry, because "the reader greps the tombstoned symbol and lands on the old entry". Here the reader lands on the amended paragraph — the correction is where the grep goes. The release note records that the amendment happened; it is not asked to carry it. 3. **Without a changeset the correction never ships.** Published tarballs are immutable; the corrected text becomes published text only on this package's next publish. With no changeset, that publish happens whenever some unrelated change happens to release `@objectstack/lint` — i.e. the fix to a *published-text* defect would depend on an unrelated event. The changeset is what makes the repair reach the surface the card is about. If a reviewer prefers the opposite call, the lever is one file (`.changeset/lint-changelog-export-claim.md`) plus the `skip-changeset` label; nothing else in this PR moves. ## What was deliberately **not** done ⛔ **The two names were not exported.** The card names two products and refuses to choose between them; the dispatching seat chose, narrowly, and only "correct the false record" was dispatched. Actually exporting `FIELD_RULE_AMBIENT_ROOTS` / `FIELD_RULE_JUDGED_ROOTS` would **widen the published surface** — the manual floor. It would carry `Clause-②: yes`, take at least a `minor`, and is the maintainer's call, not an implementer's. It is a separate card if anyone wants it, and the card's own acceptance ③ forbids doing both in one PR. This seat has no dissent to record: the amendment is the product that restores truth to text already shipped, and it is complete on its own. ## Scope `packages/lint/CHANGELOG.md` plus the changeset argued for above. No source file, no test, no `content/docs/releases/**`. In particular `packages/lint/src/validate-expressions.ts` and its test are untouched — open PR objectstack-ai#18319 is editing `validate-expressions.test.ts` and that work is not this PR's. ## Gates Derived, not recalled: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against the real change set (2 paths), which named **52** commands. All 52 were run; every exit code is in the report handed to the dispatching seat. - **50 of 52 exit 0.** 31 were green on the first pass; 16 more after `pnpm install` (a fresh worktree has no `node_modules`, so they had exited 3 — PREREQUISITE NOT MET); 3 more after the `@objectstack/lint...` build above. - **2 remain NOT MEASURED, exit 3**: `check:dual-build-cjs-loads` and `check:lean-entry-closure`. Both need a whole-repo `dist/` and say so in their own words ("this gate reads built output, and some package has no dist/"; "this gate loads BUILT entry points"). A repo-wide build is CI's run, not this PR's, and this diff contains no source byte, so neither can be moved by it. Exit 3 is neither a pass nor a finding. - `check:pm-dispatch-gates` is **not** among the derived families for these paths and was not run. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <[email protected]>
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_PATTERNis exported frompackages/spec/src/kernel/manifest.zod.ts;ManifestSchema.idandPackageSchema.manifestIdboth reference it.manifest.test.tsholds 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
formatSuggestionfromshared/suggestions.zod.ts— no new message builder.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.crmandorg.apache.supersetboth match. They are no longer a comment:MANIFEST_ID_EXAMPLESis 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, andcreate-objectstackinterpolatesderiveManifestId(projectName).os initderives from the project name too. The second half prerequisite 4 named is real and is fixed here:os init my-appsanitizes the namespace tomy_appand every template interpolated that namespace into the id, producingcom.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 importedMANIFEST_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 bymanifest_id, an installed row is keyed on it and a dependent declares it. The changeset carries the matching disposition marker and shipsminorunder the launch-window convention. Sub-question (1) is answered in the entry's ownreasonfrom 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.idreachManifestSchemaat authoring time, or only on the publish path? A failing probe, with a lit control on an existingManifestSchemaconstraint:The control proves the instrument:
manifestreally is parsed throughManifestSchemainsidedefineStack(). 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 builtManifestSchema: 21 files underpackages/qaare refused, each for the same reason — an underscore in the last segment, whichmanifest.namespaceadmits and an id segment does not.Each fixture calls
defineStackat 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, whosedistthis 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/qais 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 -Fover all twenty-one returns one hit each, so there is no installed row, nodependenciesentry 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/qaare not manifest ids, and nothing needs doing about them:packages/cli/src/commands/migrate/meta.stored-flow-resolution.integration.test.ts:62is a flow NODE id, andpackages/objectql/src/plugin.integration.test.ts:259is a plain object handed toctx.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/spectests (p,acme), four inpackages/clitests (todo,demo,bad,a/b), and the five golden corpus entries inpackages/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.tsgains an anchored source alias for the one spec subpath its tests now import, becausepnpm check:test-source-aliascorrectly 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.node scripts/pm/dispatch-gates.mjs --ranpnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/cli exec vitest run --project unitpnpm --filter @objectstack/cli typecheckpnpm --filter create-objectstack testpnpm --filter create-objectstack typecheckpnpm lint(repo-wide,eslint . --no-inline-config)pnpm --filter @objectstack/spec check:generatedapi-surface,export-origins,content/docs/referencespackages/qadogfood tierpackages/cli's integration tier is declared to CI: the diff touches no spawn entry, nobin/, notest/helpers/serve-process.tsand no driver or kernel boot path.Generated by Claude Code