Repository navigation
spec(lint): wire the six inert runtime-create doors — action / hook / report / skill / email_template / mapping - #19517
Conversation
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…ypes Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
… door directly Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…re-inert-runtime-create-rules
…re-inert-runtime-create-rules # Conflicts: # packages/lint/src/runtime-gate.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 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 b41949e6da8c54b16074a8c3f0a70c00ad714b32 && git checkout b41949e6da8c54b16074a8c3f0a70c00ad714b32
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 37ed9ae04b7b656114009378fe62cd186ab3b168 1ac5e9779b48c7d038d2073fc3e279131813b1aa && git checkout -B drift-repro 37ed9ae04b7b656114009378fe62cd186ab3b168 && git merge --no-ff 1ac5e9779b48c7d038d2073fc3e279131813b1aa
node scripts/docs-audit/affected-docs.mjs --json 37ed9ae04b7b656114009378fe62cd186ab3b168
|
Contract reviewServed-tier: ① Derived judgmentsAll readings taken in a fresh detached worktree at the head sha ( The card's first reading, re-taken through the real door.
False-refusal probes that stayed clean (good): an action bound to an object outside the snapshot ( Group C — does "dispatched and silent" honour the declaration? It is what the ruling ordered (「the wiring is the whole deliverable」), and the wiring is real: Ablations — eight, each a single-line mutation with the restore proven blob-identical to HEAD and
No wiring line can be removed with the tests staying green. A2/A7 say the group C wiring is held by one Corpus, re-taken from the built door over every example app — including Suites and gates at head: Clause-②: The merge commit, verified independently. Three-way recompute ( The restated sentence. Counted off the table at head: sixteen rows sit above the ② Semver level
③ Boundary flagsFAIL ground 1 — the FAIL ground 2 — the changeset's breaking declaration, stated in ② with its fix. The #4716 fence re-point — not a ground, but the claim is false and should be reworded. The new arm asserts, for a fenced rule on the runtime surface, Other readings, no action owed: the refusal messages for action/hook carry a prose locator as Implemented-by: VERDICT: FAIL Generated by Claude Code |
…re-inert-runtime-create-rules
… the changeset breaking The at-tier contract review returned FAIL on two grounds. Ground 1 — the `skill` door shipped a false advisory on the ADR-0109 default path and the tests pinned it green. `validateAiToolReferences` resolves into `stack.tools` and `stack.actions`; a per-write snapshot carries neither, so at that door the rule has no truthful `unresolved` verdict at all. Measured on the shipped corpus: app-showcase's only AI-exposed action is stack-level, and a skill naming it is advised unresolved at the door while the same rule over the whole stack answers clean -- an advisory that reaches Studio and prescribes what the author had already done. `skill` is held out of this landing and takes the ruling's group B treatment of `tool`, the same universe obstacle read from the other side. Both halves of the wiring are held ABSENT by pins and the measurement is kept executable beside them. Group A lands as three types. Ground 2 -- the changeset now opens with the BREAKING-for-runtime-metadata-writes banner, declares `Clause-②: no (narrowing)` and carries one ADR-0087 marker. Also corrected, flagged but not a ground: the re-pointed #4716 Q2 fence comment claimed the new arm was stronger. It is implied by the `atDoor` assertion and is strictly weaker than the clause it replaced; the comment now says so, and why the relaxation is still faithful to that fence's object-door remit. And group C's proof is recorded where a reader meets it as a table pin, not a door reading. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
`check-adr-0087-registration` refused `not-required (no-migration-prescription)` against the framed FROM/TO table the changeset carried: a changeset shipping instructions for rewriting a consumer's content cannot also claim no consumer rewrites anything. The claim was the thing that was wrong shape, not the marker. Nothing here is retired or renamed and there is no old spelling to translate: every defect now refused at the runtime door was ALREADY refused by `os build`, `os validate` and `os lint`, with the same rule id, severity and fix-it text. Only the set of doors widens. The section now states which writes join the refusal set -- the shape the sibling #19370 changeset uses -- and says plainly that the rule's own hint carries the correction at the moment of refusal, so there is nothing for `objectstack migrate meta` to reach. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsRound-2 DELTA review. Own detached worktree at the head sha ( The door, re-taken through the BUILT
One bad write and one good write per wired type, all through
I read the full envelopes, not only the rule ids: every refusal names the rule id, the path, the offending string and an actionable correction ( Corpus, re-taken from the built door over all four example apps. Each app's config loaded through the CLI's own Suites and gates at this head. Clause-②, re-measured at this head: Ablations — three, none of them the dev's two, each a single-line mutation with the restore proven blob-identical to HEAD and
Route (a), measured rather than argued.
Round 1's ground-1 measurement reproduces exactly, and route (a) reaches whole-stack PARITY on every probe. The changeset's category, judged on the merits and not on the gate's exit code. Group C's proof size is now recorded in three places — at the table row, at the string assertion, and over the block — and each statement is accurate: those two rules judge nothing at this door, so no behavioural case can tell The #4716 fence wording is now TRUE, on both claims. Contradicting one round-1 confirmation, with the measurement. Round 1 confirmed 「Twelve of the sixteen」 in ② Semver level
③ Boundary flagsServed at opus. ⭐ THE SCOPE CHANGE — holding
⛔ One of the dev's framings is wrong and should not be carried forward. The policy's second branch, 「not honourable ⇒ retired」, means Verdict on the four reasons: (1) TRUE and compiler-enforced — FLAG A — reason 4 is false, and it is recorded on #19527 as something that card 「must decide, ⛔ not inherit」. The claim is that FLAG B — this delta introduced a NEW false sentence, in the very comment the PR restated because a stale count is a defect. FLAG C — the hold-out's reproduction pin cannot tell the reader when it has gone stale. The Round-1 ground 1 — CLEARED. The false advisory does not ship. A Round-1 ground 2 — CLEARED, on the merits and not only at the gate; stated in ② with the measurements. Carried from round 1, still true, still no action owed: Implemented-by: VERDICT: PASS Generated by Claude Code |
…re-inert-runtime-create-rules
…ect a false count Test Core shards 4/6 and 5/6 were red on the previous head and neither the dev nor the review had run the package that hosts them: @objectstack/objectql declares no dependency on @objectstack/lint, so it was invisible to a dependents-only sweep, and it is where the runtime write path is exercised. Root cause, one defect in two tests: `metadata-validation-sweep.test.ts` and `overlay-precedence.test.ts` each author a `report` that binds a dataset their tenant does not declare, into a harness whose live universe is permanently empty (`find` mocked to `[]`, nothing read back). Since the report door opened, `validateChartBindings` resolves that binding and refuses it — `chart-dataset-unknown`. The refusal is TRUE: the reports really do bind nothing. `ReportSchema` refines `dataset` to required, so dropping the binding is not available — a report either binds a dataset the tenant has or it is not a report. Both harnesses now seed that dataset into the registry universe, the landed pattern from `protocol.dashboard-dataset-publish-gate.test.ts`. ⛔ No test skipped, disabled or quarantined; no rule weakened; every whitelist, hash and rejection assertion untouched. Also: `runtime-gate.ts` claimed 「Twelve of the sixteen mappings」 and 「#19474's four rows」. Dropping `skill` falsified both -- it is eleven of fifteen and three rows, counted from the table itself, and it contradicted this file's own correct 「three rows」 68 lines above. Comments ship in dist, so this shipped. The provenance note about #19370's original wording is kept, with the withdrawn-row step recorded rather than erased. Two round-2 review folds: the hold-out's LIT pin now DERIVES the door snapshot through `buildRuntimeWriteSnapshots` instead of hard-coding `{ objects, skills }` -- ablated, it now goes red when `actions` joins `RuntimeStackContext`, which is the signal that `skill` can be crossed; and the changeset headline no longer says five doors judge when two of them judge nothing. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
The other half of the red Test Core shards, same defect one package over. `rest-server-meta-read-org-scope.test.ts` and `rest-server-meta-history-diff-org-scope.test.ts` both write a `report` whose `bodyFor` binds `orders_ds`, into a stub engine whose registry double answers `listItems: () => []` for every type. That was harmless while no rule judged a report; since the report door opened, `validateChartBindings` resolves the binding and the write is refused with `chart-dataset-unknown` before the READ each file exists to exercise is reached -- six cases, all of them the `report` arm of a per-type sweep. The refusal is TRUE and `ReportSchema` refines `dataset` to required, so the fixture needs a tenant to be valid in rather than a binding to drop. Both doubles now answer `dataset` with `orders_ds`, whose measure name is exactly what `bodyFor` selects. ⛔ No test skipped, no rule weakened, every org-scope and history assertion untouched. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
`runtime-gate.ts` claimed 「Twelve of the sixteen mappings」 and 「#19474's four rows」. Counted from the table itself: fifteen rows sit above `position`, four of them context collections, so it is ELEVEN of FIFTEEN -- and #19474 landed THREE rows, not four. The figures were true at the round-1 head; withdrawing the `skill` row falsified both, and they contradicted this file's own correct 「The three rows」 68 lines above. The build does not strip comments, so this sentence ships in dist/index.js, dist/runtime.js and both .cjs. The provenance note is kept and extended rather than erased: #19370's original 「eight of the twelve」 was true of the table it was written against, and the withdrawn-row step is now recorded instead of leaving a silent jump. The figure is arithmetic over the rows above it, so the note says to recount rather than adjust.⚠️ This correction was made once before and LOST: it was uncommitted when an ablation's restore leg ran `git checkout HEAD -- runtime-gate.ts`, which discarded it silently at exit 0. Caught by reading the committed blob back instead of trusting the edit. AGENTS.md says it: commit the fix FIRST, then ablate. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…re-inert-runtime-create-rules
`check:issue-citations` reports `[allocated-but-absent]`: cards #19474 and #19370 were filed by an account since banned, so both answer 404 while their comments, timelines and landed work all still resolve. ⛔ Neither was deleted. #19474 -> #19542, the verbatim rebuild. Confirmed rather than guessed: #19542 resolves 200, its title opens `[rebuild of #19474]`, its body states the original is unreachable and tabulates the same 404s, and the PR already closes it. TWENTY-FIVE sites, not the nine the gate printed -- its own tail says the red is a LOWER BOUND because it stops at the first non-zero exit, and the rest were in the objectql and rest fixtures and the lint tests, all of which sit on the same judged `packages/**/src/**/*.ts` surface. The changeset is re-pointed too: it is not a judged surface, but it ships verbatim into CHANGELOG.md, which is the most durable reader-facing prose this repo has. #19370 is NOT re-pointed, and that is the point. It is historical provenance -- the sentence records what that card's author wrote and when it was true -- and it has no rebuild. Its PR is unreachable too (404, same ban), so naming it would only mint a second dangling reference. Taken instead is the gate's second remedy: keep the number, say in prose that it no longer resolves, and name the live record -- the merge commit a227afa, which carries that card's whole diff. ⛔ No wiring, test or hold-out touched: comment and prose only. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
The prose remedy alone is not enough: `check:issue-citations` judges every bare `#N` on an added line against the board, so keeping the hash form and explaining it still reports `[allocated-but-absent]` -- measured, exit 1 on two sites. Read the gate's own sentence again and it says what to do: a dead number dressed as a live link IS the dangling reference. So the number is kept and spelled as what it now is -- a historical card id, 19370, without the sigil -- with the reason stated inline, and the live record named as the merge commit a227afa. ⛔ Not evasion of the gate and ⛔ not a qualifier dodge: `owner/repo#N` would have skipped the probe by declaring a cross-repo reference, which this is not. The citation is removed because it no longer cites anything. Measured after: `node scripts/check-issue-citations.mjs` (the diff-scoped form lint.yml runs, with a token) exits 0 -- 15 citations judged, 15 resolve. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsRound-3 DELTA review. Own detached worktree at the head sha ( ① The red suite, and its root cause — CLEARED, and the fix is the right one. Reproduced the failure before judging the repair. Ablation A (strip all four seeds at this head, nothing else changed):
Eight cases, all of them the (a) The seeded fixtures are truthful, and that is ENFORCED rather than asserted.
The seeds are authorable metadata — strictly more truthful than the landed pattern they cite. Their dimension/measure names are what (b) Nothing was weakened. The net diff of the four files across the WHOLE PR is +100 / −3, and the three removed lines are (c) A report binding a dataset nobody declares is STILL refused. Ablation C — keep the seed, point the fixture at ⭐ The ② The 25 citation re-points, and the one not re-pointed — both choices sound. Re-pointing to #19542 is sound and rewrites nothing. Verified independently: The sigil-less ⭐ Refusing the The two surviving ③ The false sentence — counted myself, true now, and it ships.
It ships. Built ④ The two round-2 folds — both reproduced. The LIT pin. Ablation G — add The changeset headline. 「five metadata write doors that dispatched NOTHING now dispatch the rules already written for them, and three of the five judge what walks through」, then names them — What I tried that did NOT break it. Full suites at this head, exit 0 every one: ② Semver level
③ Boundary flags⛔ None of these is a ground. None is on a wire a user acts on. FLAG A — the round's own site count is one low, in the round whose item ③ is a false count. 「25 sites across ten files」 (the dispatch note and commit FLAG B — FLAG C — one dead card is now spelled two ways in one file, and full consistency was available. FLAG D — the corrected sentence still has no mechanical pin, and it has now gone stale twice in three rounds. 「eight of the twelve」 was falsified by this card's rows; 「twelve of the sixteen」 was falsified 68 lines from a comment that already said 「three rows」, by withdrawing one row. The file's own remedy — ⛔ 「recount them rather than adjusting it」 — addresses a reader, not a gate, and the figure is derivable from two constants the package already exports ( FLAG E — the FLAG F — the changeset file is still named Implemented-by: VERDICT: PASS Generated by Claude Code |
… publish door — the reading says group C, dispatched and silent (objectstack-ai#19596) Fixes objectstack-ai#19568 Clause-②: yes ## The reading, first — and it is neither of the two arms the card offered The card asks which group `datasource` belongs to: **A** (wire the rule) or **D** (retire the declaration). Measured against the ruling's own group criteria, it is **neither — it is group C**, the ledger-driven arm, which that same ruling folds into group A's card. Both offered arms are refuted, and the second refutation is the one that decided the shape of this PR. **Group D is refuted by its own criterion.** That arm is for a declaration where 「no stack collection exists to create into, so the declaration is a promise nothing can keep」. `ObjectStackDefinitionSchema.datasources` is a first-class stack collection (`packages/spec/src/stack.zod.ts:275`), the type has a live runtime create path (ADR-0015 Addendum, `origin: 'runtime'`), and `RUNTIME_CREATE_ALLOWED_TYPES` in `packages/metadata-protocol/src/protocol.ts` is derived straight from the registry entry, so `PUT /api/v1/meta/datasource/NAME` really does mint one. Retiring the flag would withdraw a capability the platform ships. **The `skill` hold-out is refuted, and this is the precedent the dispatch asked me to test against.** `skill` stayed out of the sibling landing objectstack-ai#19542 because `validateAiToolReferences` reads a skill's tool references against `stack.tools` and `stack.actions` — collections the runtime door's snapshot does not carry — so the door produced that rule's `unresolved` finding while the same rule over the whole stack produced none. Wiring it would have shipped a false advisory into Studio; the hold-out is written up on card objectstack-ai#19527. `datasource`'s only candidate rule reaches into no collection at all: `lintLivenessProperties` judges each written item's own top-level keys against that type's liveness ledger, so the door's verdict and the whole-stack verdict are the same value by construction. That is asserted in this PR as a comparison, not argued: the same written datasource judged once in the shape a per-write snapshot has and once with every collection the door omits present and populated, with an anti-vacuity leg so two empty lists cannot pass for agreement. ⇒ **wiring is honest here, and it lands as the `email_template` / `mapping` shape: dispatched and silent.** ## Evidence, re-measured on `origin/main` `1f69917c5c` (after PR objectstack-ai#19517 landed), not adopted from the card | leg | reading | | --- | --- | | registry entries parsed with a multi-line-aware scanner | 27 entries · **22** carry `allowRuntimeCreate: true` | | of those 22, entries written MULTI-LINE | **1** — `datasource`, at `metadata-plugin.zod.ts:930`. The card's mechanism holds at this commit. | | `allowRuntimeCreate: true` types named by **no** live `runtimeTypes` declaration in `packages/lint/src` | **6** — `datasource`, `external_catalog`, `translation`, `doc`, `tool`, `skill` | | lit control, same scan | declarations naming `object`: **12** | | dark control, same scan | declarations naming `zzznotatype`: **0** | | candidate rules naming `datasource` anywhere in `packages/lint/src` | **1** — `lint-liveness-properties.ts:502`, the row `{ type: 'datasource', key: 'datasources' }`, carried since objectstack-ai#4487 | | `packages/spec/liveness/datasource.json` | 12 props, **0** `authorWarn` rows (the three textual hits are prose inside notes) | | lit control, same instrument | `object.json` warns on `externalSharingModel` | Of the six, `datasource` was the only one with no carrier: the others are the ruling's group B readings, its group D retirement, and the objectstack-ai#19527 hold-out. ## What landed - `lintLivenessProperties` declares `datasource` in `runtimeTypes`, beside `email_template` and `mapping`. - `TYPE_TO_STACK_KEY` gains `datasource: 'datasources'` — ⛔ not a mapping ahead of its rule: that rule has read `stack.datasources` since objectstack-ai#4487. - `runtime-gate.datasource-writes.test.ts` carries the reading in executable form. - The registry entry itself now says where its `allowRuntimeCreate: true` is honoured, that the honouring rule is silent by ledger, and that the entry's multi-line shape is what hid it from a line-wise census. **⚠️ Dispatched and silent, on purpose.** With 0 warn keys no datasource document can be advised at this door today, so the behavioural acceptance the sibling card used — a real write refused, a good write passing — is **not available for this type**, exactly as it was not for group C. The same fence applies: the wiring is the whole deliverable and ⛔ no ledger-population work rides with it. The silence is pinned beside a lit control on the same instrument in the same process, so it can never be read as a broken dispatch or an unresolvable ledger directory. **One place this proof is LARGER than its two siblings'.** Group C's stack keys rest on a single string assertion, because with an empty warn map no behavioural case can tell `'mappings'` from a `'mapping'` typo. This file closes that gap for its own row: it drives the real rule through its ledger-directory seam over a stack built at `stackKeyForType('datasource')` itself, with the wrong-key leg asserted beside it. Ablated (`ablation-replace`, on-disk blob change proven, restore proven byte-identical to HEAD): | mutation | result | | --- | --- | | `runtimeTypes` loses `'datasource'` | 3 red — the dispatch pin, the door pin, the roster pin | | `datasource: 'datasources'` becomes `datasource: 'datasource'` | 2 red — including the behavioural stack-key case, which is what group C could not manage | ## What this does NOT reach, stated rather than left to be discovered The Setup wizard's own route — `POST /api/v1/datasources` — persists through `DatasourceAdminService.createDatasource`, which writes via `metadata.register` plus a direct `sys_metadata` row, and never reaches `saveMetaItem`. So this crossing honours the `/meta` door (REST, MCP, an AI author), not the wizard's dedicated route. The two doors enforce disjoint check sets today; that asymmetry is real, is outside this card's file surface, and is written up under Acceptance notes rather than repaired here. ## Verification - `pnpm --filter @objectstack/lint test` — 108 files, 4079 passed, 5 skipped. - `pnpm --filter @objectstack/lint typecheck` — green, test layer included. - `pnpm --filter @objectstack/spec test` — 509 files, 14895 passed. - **The publish-gate blast radius, not the rule registry's package graph**: `pnpm --filter @objectstack/metadata-protocol test` (185 passed, 3 skipped), `pnpm --filter @objectstack/objectql test` (303 files, 5050 passed), `pnpm --filter @objectstack/rest test` (194 files, 3254 passed, 1 skipped) — the packages that drive `saveMetaItem` and declare no dependency on `@objectstack/lint`. - `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated` — all 15 generated artifacts up to date; the registry comment moved none. - `pnpm lint` (repo-wide, `eslint . --no-inline-config`) — exit 0. - Derived gate families (`scripts/pm/dispatch-gates.mjs`, reconciled with `--ran`): **86 derived, 83 run green, 3 NOT MEASURED, 0 unrun**. The three are `check:dual-build-cjs-loads`, `check:i18n` and `check:type-check-debt`, each exiting **3** — PREREQUISITE NOT MET, a whole-repo build this run did not have. ⛔ Not failures and ⛔ not passes; CI builds first and runs all three. All measured at `a2596caebf`. ## Acceptance notes - **The two runtime-create doors for `datasource` enforce disjoint check sets.** `assertDatasourcePoolSupported`'s own docblock says it is 「Called at every door a `pool` block can come in through — the Setup wizard's create/update, the boot-time auto-connect pre-pass, and the driver factory itself」, and the `/meta` write door is a door it does not list: a datasource minted there is Zod-parsed and gated by the authoring rules, but never sees `assertValidConfig`, `assertDatasourcePoolSupported` or the code-origin collision refusal that `createDatasource` performs before persisting. The record lands in `sys_metadata` and is met at connect time instead, which is the outcome `createDatasource`'s own comment says it exists to prevent. Outside this card's file surface (`packages/services/service-datasource`, `packages/metadata-protocol`) and a different defect class, so it is reported for filing rather than repaired here. - **Group D's criterion is measurably false for one of its two members.** The ruling retires `doc` and `external_catalog` because 「no stack collection exists to create into」. At `1f69917c5c` there is no `externalCatalogs` collection, so that holds for `external_catalog` — but `ObjectStackDefinitionSchema.docs` exists (`stack.zod.ts:431`). Whoever takes the group D retirement should re-read the premise for `doc` before flipping the flag. Carrier: the group D retirement card. Noted, not filed. - **`lintLivenessProperties` re-reads the ledger directory on every call** — `resolveLivenessDir()` plus one `readFileSync` per governed type, memoised only within a call, and the gate runs its rules twice per write. Bounded today to the three types that dispatch it, so it is an observation, not a card. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…lares its object (objectstack-ai#19857) Fixes objectstack-ai#19586 · **one row**, not the pattern: measured, no other harness in `packages/objectql/src` pays an empty-universe cost today (census below), and no shared seed could serve them, because each harness's universe is whatever its own fixtures name. Clause-②: no ## What changed One file: `packages/objectql/src/metadata-validation-sweep.test.ts`, test-only. - **The `dataset` row is filled.** It used to print `dataset yes - - no fixture (skipped)`. It now reads `dataset yes ok ok`: a valid dataset is accepted and an invalid one is rejected. - **The harness universe gains the object the dataset is over.** `makeProtocol` already seeded `sweep_account_metrics` (added for the `report` door in objectstack-ai#19542). It now also registers `sweep_account` through `registry.registerObject`, which the gate reads through `listItems('object')`. Without it the dataset fixture is refused `object-reference-unknown`, the wall objectstack-ai#19542's dev hit and reverted. - **The tenant is declared once.** `SWEEP_ACCOUNT` and `SWEEP_ACCOUNT_METRICS` are named constants. `makeProtocol` seeds clones of them, and the `object` / `dataset` rows publish them as their `valid` documents, so the seeds cannot drift from the rows. They are deliberately not read out of `FIXTURES`: that spelling made removing the `dataset` fixture crash every row on the missing key, where the skip note should come back. - **The invalid leg now asserts which door refused.** The runtime author-time gate throws the same envelope as the schema door (`INVALID_METADATA`, 422, `issues[]`), so the old check could not tell them apart. A refusal now counts only when it has no gate `rule` and a schema issue names the fixture's `invalidatedField`. Gate issues always carry `rule` and a stack-rooted path (`datasets.NAME.object`). - **The `skill` row's `invalidatedField` is now `tools`, not `description`.** Measured: the schema's only issue on that payload is at `tools`, and `description` is optional. This relabel is the one row the new assertion forced. - The `report` comment "the bound dataset is the fixture directly above" was false on `main`: it landed in objectstack-ai#19517 while the fixture it named was reverted. It is true again. ## One row or the pattern: the census (at `afc3b6492`) - 68 test files in `packages/objectql/src` reference `ObjectStackProtocolImplementation` / `saveMetaItem`. **9** of them both call `saveMetaItem` and carry an empty-universe mock (`find` resolving `[]`, `listItems: () => []`). - One of the 9 is this file. The other 8 (`overlay-precedence`, `protocol-commit-history`, `protocol-destructive`, `protocol-lock-enforcement`, `protocol-meta-types-rich`, `protocol-meta`, `protocol-publish-package-drafts`, `protocol-registry-shadow`) run **202/202 green with 0 `authoring advisory` lines and 0 `*-unknown` / `*-unresolved` rule ids**. Control: the same log carries 19 other `[Protocol]` warn lines from the same `dist`, so an advisory would have shown. - Widened to the whole package (304 files): 7 advisory lines, none from a reference-resolution rule (`approval-expression-no-empty-policy` x4, `flow-multi-write-unfiltered` x3, both body-shape rules), and 0 `*-unknown` / `*-unresolved` ids. - So the pattern's cost is future only, paid when a later door opens, and it cannot be seeded ahead: each harness's universe is the names its own fixtures reference. Blind spot: the census matches the mock spellings named on the card. A harness that builds its engine with no registry at all would also have an empty universe, and would not be counted here, only covered by the package-wide advisory count above. - `packages/rest` harnesses belong to `domain:cli` and are **listed, not edited**. Five carry the same mock spelling and drive `saveMetaItem`: `meta-compound-save-force-parity`, `meta-compound-save-mode-parity`, `public-form-lookup-filter-lowering`, `public-form-lookup-picker`, `public-form-routes.stored-row`. The two objectstack-ai#19517 already seeded are `rest-server-meta-history-diff-org-scope` and `rest-server-meta-read-org-scope`. ## Evidence All at `b244ec482d` unless marked. - **Before, `afc3b6492`:** `dataset yes - - no fixture (skipped)`. - **Card premise reproduced** (fixture added, no object seed): `dataset yes fail ok valid: INVALID_METADATA: dataset/sweep_account_metrics failed author-time validation: 1 issue — datasets.sweep_account_metrics.object [object-reference-unknown]`. - **After:** every executed row `ok ok`. The invalid dataset's issues are exactly `[{"path":"measures","message":"Invalid input: expected array, received undefined","code":"invalid_type"}]`, from the schema, no `rule`. - **Ablation (owed): remove the `dataset` fixture, and the skip note comes back.** Run through `scripts/ablation-replace.mjs --delete` (WRAP mode, restore trap). Anchor 1 to 0, blob `a9f40f14c53e` to `3ff6f86a0252`. Row: `dataset yes - - no fixture (skipped)`; `object` / `report` still `ok ok`. Restored: blob equals HEAD `a9f40f14c53e`, `git diff HEAD` empty. - **Reverse check of the new assertion (one-off, not kept).** The invalid dataset is made schema-valid but pointed at an undeclared object, so only the gate can refuse it. - (A) With the new check: `dataset yes ok fail invalid: 422 is not the schema refusing \`object\`: [{"rule":"object-reference-unknown",...}]`, exit 1. - (B) Same fixture, check reverted to envelope-only: `dataset yes ok ok`, exit 0, a false pass. - Both legs were restored to blob equals HEAD, with `git diff HEAD` empty. - `pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2`: **304 files / 5072 tests passed**, exit 0. - `pnpm --filter @objectstack/objectql typecheck`: exit 0. `check:test-typecheck` OK (40 files / 234 errors, all in its ledger). This file is in `tsconfig.test.json`'s program (`--listFiles`), has 0 error lines, and has no ledger entry. - **Gates:** `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 54 commands, and all 54 exit 0. Two first answered exit 3 `PREREQUISITE NOT MET` (`check:dual-build-cjs-loads`, `check:type-check-debt`); both went green after `turbo run build --filter='./packages/*' --filter='./packages/*/*'` (72/72). The dist sweeps were rerun at full population. `--ran`: **54 derived, 54 run, 0 NOT-MEASURED, 0 UNRUN**, exit 0. - **Live `node scripts/check-issue-citations.mjs`:** exit 0, `no issue citations added ... (0 file(s) read)`. Test files are that gate's declared deferred surface, so this is not a clearance of the new citations. The two new targets were checked by hand: objectstack-ai#19143 answers 200 (closed, completed) and objectstack-ai#19586 answers 200. - **eslint, narrowed to the one file:** `--no-inline-config --format json`: 1 file, 0 errors, 0 warnings. The file's resolved config (`--print-config`) has no `parserOptions.project` / `projectService`, so linting is not type-aware and the edit cannot move a verdict on any untouched file. The repo-wide `pnpm lint` is CI's. - **`skip-changeset`, measured:** `@objectstack/objectql` ships `files: [dist, README.md, CHANGELOG.md]`. Five symbols unique to this diff (`SWEEP_ACCOUNT_METRICS`, `invalidatedField`, `sweep_account_metrics`, `metadata-validation-sweep`, the new note text) hit 0 files there, while the control `class SchemaRegistry` hits 2. ## Bounded in-place fix, declared The generic invalid-leg assertion and the `skill` relabel go beyond the one row. All four conditions hold: 1. It is the class this card hit: a sweep verdict produced by an author-time refusal instead of the schema, and `skill` was a live instance of a row refused for a reason other than the one it names. 2. It is mechanical, with its shape pinned by the measured issue list of every executed row. 3. No other claim holds this file. 4. It adds no new verification surface: the same test, the same gate family. ## Acceptance notes (not filed) - Seven other runtime-creatable types still print `no fixture (skipped)`: `book`, `datasource`, `doc`, `external_catalog`, `mapping`, `position`, `seed`. Each is its own fixture, with its own universe to measure first. Triage graded this shape as a coverage gap, not a finding. - Seven `FIXTURES` entries are never executed, because their types are not `allowRuntimeCreate`: `field`, `validation`, `approval`, `job`, `profile`, `role`, `agent`. That is the failure mode this file's own `api` note describes ("looking like coverage while asserting nothing"). Dead code, noted only. --- _Generated by [Claude Code](https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19542
Clause-②: no
os-samaccount that filed it was banned, soFixes #19474now readsFixes #19542, its verbatim rebuild. The claim, the round-1 FAIL record5756052587and every correction still read on the old card and are linked from #19542. ⛔ Nothing about the delivery changed.⭐ Corrected by the owning seat after the round-1 contract review (record
5756052587): this lands FIVE wired types, not six —skillis HELD OUT. The original sentence is struck through rather than deleted.Six metadata types declaredallowRuntimeCreate: trueand reached zero author-time rules at the runtime publish gate. This wires them,Six metadata types declared
allowRuntimeCreate: trueand reached zero author-time rules at the runtime publish gate. This wires five of them —action,hook,report,email_template,mapping— and holdsskillout as a reading, under the ADR-0049 ruling on #19275 (5754204885, batch #203 item 4, letter B — 「declared ⇒ honoured; not honourable ⇒ retired」), groups A (action·hook·report·skill) and C (email_template·mapping), as one card.The first reading the ruling asked for
The ruling carried forward one NOT MEASURED item unchanged — 「whether a wired rule fires on a real write」 — and made acceptance behavioural. Here it is, per type, each with the test that proves it. All legs go through the real door (
runRuntimeAuthoringRules), never through a rule called directly.actionvalidateStackExpressionsexpression-invalid)⭐ LIT — an action whose visible CEL does not parse is REFUSED,⭐ LIT — an action bound to an object, naming a field it has not got, is REFUSEDcrm_convert_leadverbatim)hookvalidateStackExpressionsexpression-invalid)⭐ LIT — a hook whose condition names a field the object has not got is REFUSED,⭐ LIT — a hook whose condition does not parse is REFUSEDshowcase_audit_task_completionverbatim)reportvalidateChartBindings,validateEmptyCombinators,validatePresetComparandschart-dataset-unknown,chart-dimension-unknown,filter-empty-combinator,filter-preset-comparand)⭐ LITcases, one per rule idcompleted_tasksverbatim, a filter-carrying member of the corpus)skillstack.tools/stack.actionsand the door carries neither, so the corpus's own AI-exposed stack-level action reads as a FALSEunresolvedadvisory and a good write does NOT pass clean. Takes the ruling's group-B treatment oftool: a reading, not a wiring. Both halves of the wiring are held absent by pins.⭐ DARK — a skill write dispatches NOTHING, and has no stack key·⭐ LIT — the reason, reproduced: one skill, one rule, two universesemail_templatelintLivenessPropertieswrites DO dispatch the ledger rule+the rule judges NOTHING todayshowcase_task_done_emailverbatim)mappinglintLivenessPropertiesshowcase_inquiry_feedverbatim)skillis held out. That is the ruled end state, not a shortfall — see the two readings below. Nothing here is asurfaces/runtimeTypesfield that merely changed.Each control was shown to be load-bearing
A green control that would be green anyway proves nothing, so each declaration was reverted and the tests watched. Every mutation is proven on disk (anchor count + blob hash) and every restore proven byte-identical to
HEAD, viascripts/ablation-replace.mjs.validateStackExpressionsruntimeTypesback to['flow']actionandhookcasereport: 'reports'fromTYPE_TO_STACK_KEYreportcasevalidateAiToolReferencesmember back to the['flow']defaultskillcaselintLivenessPropertiesback toCLI_ONLY+surfaceReasonobjectonlintLivenessProperties(against the re-pointed #4716 fence)Measured before crossing, at the door's own snapshot shape
Every item of these types shipped in this monorepo, pushed through the gate's real baseline/candidate differential:
actionhookreportemail_templatemappingskillskillis held out of this landing, so no budget is owedTwo readings that are part of the deliverable
1.
email_templateandmappingare wired and SILENT.lintLivenessPropertiesis ledger-driven and skips a type whose warn map is empty.packages/spec/liveness/email_template.jsonis 13 props / 0 warn keys,mapping.jsonis 7 / 0 — lit control on the same instrument, same run:tool.json6/1,object.json35/1. The ruling dispatched the wiring and ⛔ no ledger-population work: 「the empty warn maps stay empty until a real property needs a row — zero pull, the wiring is the whole deliverable」. Both halves are pinned — that the rule is dispatched, and that it judges nothing — plus a lit control proving the same instrument fires in the same process on a ledger that does warn, so the two zeros can never be confused with a broken dispatch or an unresolvable ledger directory.2. A
skillwrite is judged with a PARTIAL tool universe.collectToolUniverseunions the platform tool registry ∪stack.tools∪ the action family fromstack.actionsand every object'sactions. A per-write snapshot carriesobjects(so an object-levelaction_NAMEresolves) but neithertoolsnoractions, so a skill naming a stack-level declared tool reads as unresolved at this door while it is clean on the whole stack. Two things bound it: ADR-0109 states the default authoring path declares no tool records at all, and this member iswarning-tier throughout — it advises and can never refuse a publish. Pinned in both directions, so it can only change deliberately. Closing it properly means carryingtools/actionsinRuntimeStackContext, which is also an edit to@objectstack/metadata-protocol's routing table — outside this card's file surface and its own decision.The fences, and what deliberately did not cross
action/hookdo NOT dispatch the reference-integrity suite. It carries the four body-writes members, which parse authored JS throughtypescript/sucrase— and an action/hook write is precisely the snapshot that would carry a body for them to parse. That is the one crossing that turnsruntime-lazy-deps.test.tstier 1 («the parsers load NEVER») from a standing fact into a red. Pinned as a DARK case;runtime-lazy-deps.test.tsis green.validateActionNameRefs/validateActionDispatchContractdo not cross either — they readstack.actionsas a resolution universe for a view's button wiring, so an action write can only make a reference resolve, i.e. only REMOVE findings, which the differential already discards.lintLivenessPropertiesstill does not reach the OBJECT door.RUNTIME_OBJECT_ADVISORY_VOLUMEis about ~8 advisories per object write rendered in Studio;objectis not declared, so that reason is untouched.validatePresetComparandsandvalidateEmptyCombinatorscross toreportTOGETHER ([finding] The fourviews[]visibility-predicate rules are CLI-only — a Studio/REST/MCPviewwrite bypasses all of them; if they move to runtime-publish, they must move together #7220): both judge the same authored filter literal on the samereportssurface, so an author refused for a bad comparand and waved through for a literal$and: []on the same report could not predict the door.reportandskilleach reach exactly ONE suite member, pinned by name.One pin was re-pointed, and it is the interesting one
The #4716 Q2 fence in
runtime-gate.object-writes.test.tsasserted that each of six advisory-tier rules is absent from the object door and carries a substantivesurfaceReason. Until now every fenced rule happened to be off the runtime surface entirely, so thesurfaceReasonclause was a faithful proxy for the fence.lintLivenessPropertiescrossing for two non-object types broke the proxy without touching the thing it stood for. The fence now asks the question directly — a rule on the runtime surface must not declareobjectinruntimeTypes— which is the stronger of the two arms, mechanical where asurfaceReasonis prose that goes stale. ⛔ Neither arm is a way around the fence, and the ablation above confirms it still refuses an object crossing.Verification
pnpm --filter @objectstack/lint test— 107 files / 4074 tests pass (head1ac5e9779b);pnpm --filter @objectstack/lint typecheck— clean (test layer compiles undertsconfig.test.json).@objectstack/metadata-protocol's five runtime-gate suites — 63 tests pass.pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'— 72/72.1ac5e9779b, (scripts/pm/dispatch-gates.mjs) run, reconciled with--ran, 0 NOT-MEASURED, 0 UNRUN, every one recording an exit code.eslint . --no-inline-configover the whole repo — 6971 files, 0 errors, 0 warnings (head1ac5e9779b) (not a narrowing: the full sweep ran).pnpm check:nul-bytesgreen, plus a direct control-byte scan of every changed file.origin/main(c9b23cd) after fix(lint): give the liveness walk a seam of its own, and report a ledger that could not be read #19480 landed inlint-liveness-properties.ts, refreshed install + full build, and re-ran the above.Acceptance notes
Observed while measuring, ⛔ not fixed here, routed to the PM:
loadWarnMap在按类型的 liveness 账本文件缺失或 JSON 坏掉时**静默返回空 map** —— 丢一个文件就关掉该类型的全部作者告警,而目录缺失那一层是响的 #19276'sliveness-ledger-unreadablecannot reach the runtime publish door. Measured withmapping.jsoncorrupted: the whole-stack rule emits["liveness-ledger-unreadable"]while the runtime door emitserrors: [],advisories: [],rulesRun: ["lintLivenessProperties"]. The finding is stack-independent, so it appears identically in the gate's baseline and candidate passes and cancels in the differential. Not introduced here — but before this card the rule never ran at that door, so there was nothing to cancel. The signal says 「this rule's silence about this type means nothing until it is fixed」 and at this door it is itself silent.actionbinds its object withobjectName; theobjectspelling is an alias the strict schema renames one layer earlier, so the door judges an object-bound action's predicate with full field resolution and an object-less one for syntax only. Measured, correct, and not a gap — recorded so the next reader does not re-measure it.Generated by Claude Code
Seat corrections, 2026-09-21T06:40Z
The round-1 at-tier contract review (
5756052587) returned FAIL on two grounds; both are addressed at head72de2b946301aa59f624da807ae7fc383e82b81e, and this body — written once at creation, per the dev-writes-it-once rule — is corrected here by the owning seat rather than by the dev.skillis held out. Group A lands as three types, not four. The dev took the review's route (b) over route (a) and its reasons are on card spec(lint): wire the inert runtime-create rules foraction/hook/report/skill/email_template/mapping— six types, one edit (ruling #203/4 group A+C) #19474; whether that is inside the ruling's logic or a scope reduction its author should decide is the round-2 reviewer's question, ⛔ not settled here.**BREAKING for runtime metadata writes**,Clause-②: no (narrowing), and oneadr-0087: not-required (no-migration-prescription)marker.not-requiredagainst this body's framed Migration table (the feat(runtime)!: 退役 ctx.user 的 roles 别名,positions 成为唯一拼法 (#6011) #6048 shape). The table was replaced with prose — the content has no FROM to translate — ⛔ not the category swapped to get past the gate.atDoorassertion and is strictly weaker than the clause it replaced. ⛔ No code change: the wording was what was false.TYPE_TO_STACK_KEYkey foremail_template/mappingreds exactly one string pin and no behavioural case, because those rules judge nothing at that door.Seat corrections, round 3 — the red suite and where it came from
Round 2's at-tier review returned PASS (record
5757740876), but CI on that head was red:Test Coreshards 4/6 and 5/6 failed, andTest Corefailed with them. ⛔ The PR was not landed on the PASS. It is recorded here because the miss is structural rather than careless.It was this PR's, measured before anything was touched: on
origin/main3e8e2b0d6dall six shards read success; on the PR head two failed.One root cause, four test files, two packages — and it is this card's own door working correctly. Each file authors a
reportbinding a dataset its harness never declares, into a universe that is empty by construction (findmocked to[];listItems: () => []). Since the report door opened,validateChartBindingsresolves that binding and refuses the write withchart-dataset-unknownbefore the assertion each file exists to make. ⭐ The refusal is true — those reports really do bind nothing — andReportSchemarefinesdatasetto required, so dropping the binding was never available: a report either binds a dataset the tenant has, or it is not a report. The fix seeds that dataset into each harness's live universe, the landed pattern fromprotocol.dashboard-dataset-publish-gate.test.ts. ⛔ No test was skipped, disabled or quarantined; every whitelist, hash, org-scope, history and rejection assertion is untouched, and a report binding a dataset nobody declares is still refused.@objectstack/objectqland@objectstack/restdeclare no dependency on@objectstack/lint— they reach the gate through@objectstack/metadata-protocol. ⇒ for a card that widens a publish gate, the blast radius is every package that drivessaveMetaItem, ⛔ not the package graph under the rule registry.packages/lint/src/; this round necessarily reached two test files each inpackages/objectql/src/andpackages/rest/src/. All four are test-harness fixtures made truthful — no production code, no rule touched — which is the standard shape for a door-widening card (#15254, #19143 did the same).⛔ One adjacent repair was declined:
metadata-validation-sweep.test.tsstill printsdataset: no fixture (skipped), and adding that fixture surfaces a pre-existingobject-reference-unknownfrom the same empty-universe condition. It was reverted and filed as its own card rather than carried here.The false sentence, corrected — and it had to be corrected twice
runtime-gate.tsclaimed 「Twelve of the sixteen mappings」 and 「#19474's four rows」. Counted from the table rather than by eye: fifteen rows aboveposition, four of them context collections ⇒ eleven of fifteen, and this card lands three rows. Both figures were true at the round-1 head; withdrawing theskillrow falsified them, and they contradicted this same file's correct 「The three rows」 68 lines above.dist/index.js,dist/runtime.jsand both.cjs.⭐ The correction was made once, lost, and made again: it was still uncommitted when an ablation's restore leg ran
git checkout HEAD -- runtime-gate.tsand discarded it silently at exit 0 — the hazardAGENTS.mdnames in as many words («commit the fix FIRST»). It was caught only by reading the sentence back out ofgit show HEAD:…instead of trusting the edit, and it is now confirmed present in the built bundles. The provenance note is kept and extended, ⛔ not deleted: #19370's 「eight of the twelve」 is recorded as true of the table it was written against, and the withdrawn-row step is recorded instead of leaving a silent jump.Seat corrections, round 4 — every citation in this diff now resolves
Round 3 was red on
Lint & Repo Gates: the issue-citation gate reported[allocated-but-absent]— 「minted and absent from the board」 — because the account that filed cards #19474 and #19370 was banned. ⛔ Neither issue was deleted;GET/PATCHon them answer 404 while their comments and timelines still resolve, and they vanish from label listings. Card #19474 was rebuilt verbatim as #19542, which is what this PR closes.The fix was bigger than the ten lines the job printed, and the job said so
The gate stops at the first non-zero exit, and its own tail states 「the red above is a LOWER BOUND on the number of problems in this tree, not a count」 and that the gates behind it are NOT MEASURED. ⇒ the dev enumerated every
#NNNNthe diff adds — 11 distinct numbers — and probed each against the API rather than trusting the printed list. Result: 25#19474sites across ten files, not nine. All 25 re-pointed to#19542.⭐ ⛔ Not a guess, and verified before editing rather than after: #19542 resolves, its title opens
[rebuild of #19474], its body states the original is unreachable and tabulates the same 404 readings, and this PR already closes it..changeset/**is not a judged surface, so the gate would never have caught it — but that text ships verbatim intoCHANGELOG.md, and a dangling number there would outlive the card.#19370is NOT re-pointed, and two assumptions were corrected by measurementThat citation is historical provenance — what #19370's author wrote, and when it was true. Re-pointing it would rewrite history to satisfy a gate.
security-role-wordcrosses to the runtime publish gate, whole (#19370) #19486 — the obvious live record to name — also answers 404, filed by the same banned account. Naming it would have minted a second dangling reference to fix the first. The merge commita227afa415f596269ed36aae0a0631c84270ccc9is named instead: it is in history, carries that card's whole diff, and cannot rot.#19370and explaining in prose that it no longer resolves still exited 1 with two[allocated-but-absent]findings — the gate judges every bare#Non an added line against the board regardless of surrounding text. Reading its own sentence again resolves it: a dead number dressed as a live link IS the dangling reference. So the number is kept and spelled as what it now is — a historical card id, without the citation sigil — with the reason inline and the commit named.⛔ The
owner/repo#Nqualifier was explicitly not used: it makes the gate skip probing by declaring a cross-repo reference. This is not one, and using it to buy silence would be evasion.⇒ the two
[#19370]citations that remain inruntime-gate.tssit on lines this diff does not touch — they are #19486's landed text, outside the gate's diff scope and ⛔ not this PR's to rewrite.Verification on this head
check-issue-citations(the diff-scoped formlint.ymlruns) — 15 citations judged, 15 resolve, exit 0.--self-testexit 0, 73 cases. All 63 derived gate families re-derived and re-run in ONE sweep, reconciled with--ran: 0 NOT-MEASURED, 0 UNRUN, none exiting 3.@objectstack/rest— the package shard 5/6's six failures lived in — 194 files / 3254 pass, and@objectstack/objectql(shard 4/6) 303 / 5050; ⛔ neither assumed from the earlier fix, both re-run here. All five ablations re-run with unchanged red counts, and ⭐ run only after the citation fix was committed and read back out ofgit show HEAD:…— the sequencing that lost a correction one round earlier.Generated by Claude Code