chore(spec): advance the authorable-surface deletion-gate anchor to main tip (#17356 letter A) - #18485
Conversation
…ain's tip `gen:authorable-surface-base` re-anchors the committed baseline on this branch's own merge base with origin/main. The deletion gate compares against the baseline at that merge base, which a commit inside a feature PR cannot rewrite — so the advance only takes effect once it is on `main`, which is why it lands here on its own rather than inside the PR that needs it. Anchor: 53ef057 (7836 keys) -> 85c6d76 (7804 keys). Generated output only; no schema, no root set and no other file is touched. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: ① Derived judgments
② Semver levelNone — ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Landing provenance — PR #18485, card #17356 letter A, head
|
| reading | value |
|---|---|
reviewer transcript "model":"…" |
91 / 91 claude-fable-5-1 |
control — an os-dev round in the same container |
297 / 297 claude-opus-5 |
| pair | Implemented-by: claude/issue-17356-anchor-advance-only · Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6 |
Record adopted verbatim (comment 5700735883); ⛔ a record is adopted whole or voided whole.
⚠️ An isolation near-miss on this review, disclosed because it is mine
While this review was running I sent it a steering message that belonged to a different PR's round (#18482's Check Changeset gate). That was a misaddressing by this seat, and an isolated at-tier review is supposed to be fed the card, the rulings and the PR — nothing else.
I measured the ordering in the reviewer's own transcript rather than assuming it:
line 161 2026-09-16T16:12:20.387Z assistant tool_use SubagentHandback (the record, 14318 B, VERDICT present)
line 163 2026-09-16T16:12:21.506Z queued_command "The `Check Changeset` red now on your head `642da7fda5` …"
⇒ the record was composed and handed back 1.1 seconds before the stray message entered the transcript. The reviewer, asked to say so if anything of mine had influenced it, independently reported the same ordering and added that its one post-handback read belongs to the other lane and should be discarded with the message. ⇒ the record stands on its own evidence (all of it dated 15:57:56Z–16:10:45Z), and the record names no changeset file and no #17884. I am recording the near-miss rather than the reassurance: had the timing gone the other way by two seconds, the correct act would have been to void this record wholesale and re-run the review.
落地前检三条
| check | reading | |
|---|---|---|
| ① | at-tier PASS in case, naming this head | record 5700735883, Head-sha: 1c331e94a257d8e66d0a4e1f447ec53e6c77a891 |
| ② | check-clause2-carriers.mjs --pair 18485, re-read after the carrier strip |
exit 0 — declaration readable in the fixed spelling, both carriers agree, a review of record names this head |
| ③ | every check green, latest run per check NAME | read after the ready flip, below |
check-governed-merges.mjs --pr 18485 → 0 of 1 path hits the register (5 surfaces) ⇒ NOT governed, derived on the FINAL file list.
Check Changeset (#18446) and a ready flip starts Governed Surface Queue Guard — a pre-strip green is not the reading the queue sees.
⭐ What this landing settles that the ruling could not
The ruling that produced letter A carried an inference nobody had tested: that an anchor-only advance would not red the authorable-key deletion gate. It is now measured true — the six required contexts are success on this head, and the reviewer re-derived why: the deletion gate runs inside Build Core and TypeScript Type Check, and the branch emits the same surface as its merge base, so the gate had nothing to adjudicate.
The stronger reading is item 2. The reviewer regenerated the anchor itself in its own worktree at the merge base and got a byte-identical file (cmp exit 0, blob 49fd6af5…), with two dark controls that differ (cmp exit 1 against the base anchor and against dc98ee22a3's). ⇒ 「regenerated on its own merge base」 is a measurement here, ⛔ not a claim, and a cherry-pick is positively ruled out: head carries 32 ObjectGanttProps|ObjectMapProps|ObjectTreeProps keys that ui.json only gained after dc98ee22a3's base.
Key delta fully accounted: 1023 arrived, 1055 left, net −32, and head's 7804 keys equal the aggregated live shards at the merge base — 0 shed, 0 invented. Keys did arrive, and every one is a key main's own surface holds; the floor moved forward, which is the ruled act.
Residue filed before enqueue: none, and that is a reading
Every ③ flag in the record is answered with 「Nothing to file」 or 「for a successor」, each with its population. ⛔ No card is filed to look diligent.
What this landing does NOT settle
The anchor now equals main's live surface at the merge base, and main has moved 7 commits past it (none under packages/spec/authorable-surface/** or packages/spec/src/**). If a surface-changing PR lands first, the anchor trails by that PR — which build-schemas.ts documents as the normal, non-error state. ⛔ No regeneration is owed on that account, and ⛔ this landing makes no claim about letters B and C or about PR #18131, whose own anchor differs (baseRev b9598e9c, 992/1056).
Generated by Claude Code
…gistered kind schemas (objectstack-ai#18131) Fixes objectstack-ai#17356 Clause-②: yes ## What was wrong `computeSurfaceReachability()` in `packages/spec/scripts/build-schemas.ts` — the whole of check (c) **proof 2** — built its BFS root set from `listMetadataTypeSchemaTypes()`. That function unions `BUILTIN_METADATA_TYPE_SCHEMAS` with the `EXTRA_METADATA_TYPE_SCHEMAS` overlay and, per **objectstack-ai#6245**, pointedly does **not** enumerate `UNREGISTERED_KIND_SCHEMAS`. `connector` lives in that third map. So the BFS never started from it, `integration/DataSyncConfig` — two hops away, through `connector.syncConfig` unwrapped once through `optional` — answered `null`, and a bare deletion of one of its baseline lines was waived as "an over-collected entry, never parsed against a metadata document". `stack.connectors[]` and `PUT /api/v1/meta/connector/:name` parse a real metadata document through that def on every boot. The gate's own docblock names this as the dangerous direction: a false "reachable" demands a tombstone too many, **a false "unreachable" would waive one silently**. The tree held exactly that false unreachable. ## What this does NOT do It does **not** add `connector` to `listMetadataTypeSchemaTypes()`. Two different questions wear the same words, and the gate conflated them: - **"is this a REGISTERED metadata type?"** — what `listMetadataTypeSchemaTypes()` answers, correctly. Enrolling the unregistered kinds there would grant a status objectstack-ai#6245 deliberately withheld (enum member, registry entry, create seed, a place in the objectstack-ai#4001 campaign count), and objectstack-ai#2657's B/C decision stays open and unprejudged. - **"is there an AUTHOR who could be authoring against this def?"** — the only question a *reachability* root set asks, because the sole consequence of `null` is waiving a tombstone on the grounds that nobody can receive the prescription. So the gate now enumerates its own reachability root union — `reachabilityRootTypes()`, reading `listUnregisteredKindSchemaTypes()`, which exists (objectstack-ai#6931) so a check can ENUMERATE that map and for nothing else, and whose listing grants nothing. `listMetadataTypeSchemaTypes()` is unchanged, and `packages/spec/src/kernel/metadata-type-schemas.ts` is untouched by this PR. ## Measured, on `c548dea2` **The waiver, both ways.** The card's literal repro key (`integration/DataSyncConfig:schedule`) already landed on `main` with the objectstack-ai#16320 retirement, so the demonstration deletes a live sibling key under the same def — `timestampField`, removed from `DataSyncConfigSchema` and from `authorable-surface/integration.json`, no tombstone, no registry entry. The gate judges the DEF half of the key (`key.slice(0, key.indexOf(':'))`), so this is the identical code path. | run | `check:authorable-surface` | `gen:schema` | verdict printed | |:---|:---|:---|:---| | before (`ca78860`) | exit **0** | exit **0** | `integration/DataSyncConfig:timestampField — def not reachable from the 26 metadata-type roots` | | after | exit **1** | exit **1** | `integration/DataSyncConfig:timestampField — def reachable from the metadata-type roots; the entry at ca78860 was LIVE (never tombstoned).` | Both legs restored from `HEAD` and proven byte-identical with `git hash-object` against the `HEAD` blob, with `git diff HEAD` empty. **Root-set delta.** 26 roots to 30; the four added are exactly `analytics_cube`, `connector`, `sharing_rule`, `webhook`; none dropped. **Closure delta (the positive control, and more than the card scoped).** Over the 1523 emitted defs, comparing the verdict map computed from the old root set against the new one: - **0** defs went from reachable to `null` — a root set that grows never shrinks a closure. `ObjectSchema` (`data/Object`) stays `root-graph`. - **17** defs stop being waivable (`null` verdicts 1035 to 1018): 16 to `root-graph` and `shared/FieldMapping` to `derived-clone`. So the answer to "is `integration/DataSyncConfig` the whole of the gap" is **no** — it is 1 of 17. The other 16 are the connector / sharing-rule / analytics-cube families: `data/CubeJoin`, `data/Dimension`, `data/Metric`, `integration/CircuitBreakerConfig`, `integration/ConnectorAction`, `integration/ConnectorFieldMapping`, `integration/ConnectorHealth`, `integration/ConnectorInstanceAuth` and its four auth branches, `integration/ConnectorTrigger`, `integration/HealthCheckConfig`, `integration/RetryConfig`. - **7** more sharpen from `derived-clone` to `root-graph` (`automation/Webhook`, `data/Cube`, `integration/Connector`, `integration/DeclarativeConnectorEntry`, `integration/WebhookConfig`, `security/CriteriaSharingRule`, `security/SharingRule`) — already refused before, refused now, with a truer reason printed. Nothing in the committed baselines moves: the waiver only fires on a deletion, so `check:authorable-surface` on the pristine tree is green before and after, and `pnpm --filter @objectstack/spec build` leaves the working tree clean. **Ablation.** With `reachabilityRootTypes()` swapped back for `listMetadataTypeSchemaTypes()` at the one call site — mutation proven on disk by anchor counts (1 to 0 and 0 to 1 on the two spellings) before the run, and the gate is executed by `tsx` from source with no `dist` in between — the new pin goes red on `expect(status).toBe(1)` receiving `0`: the deletion is waived again. Restored from `HEAD`, hash-verified. ## The pin `packages/spec/scripts/build-schemas-check-mode.test.ts` gets one case reading **both directions off one seeded state**, because either alone is satisfiable by a gate that is simply wrong in the other: "always reachable" passes the first assertion and destroys proof 2; "always unreachable" passes the second and restores the defect. It is read **twice**. Once with `OS_EAGER_SCHEMAS=1` — the way `gen:schema` and `check:authorable-surface` actually run, and the only regime where `reachableVia()` can answer `root-graph` at all; without the flag `lazySchema()` hands back a Proxy, `zodByDefKey` holds the Proxy while the walk visits the resolved target, and the same verdict arrives through the derived-clone bridge. The verdict is what the gate acts on, so the verdict is pinned in both regimes and the wording only in the eager one. The `beforeAll` fixture guard asserts `connector` is still absent from `listMetadataTypeSchemaTypes()` and still present in `listUnregisteredKindSchemaTypes()` — acceptance 4 stated where it fails rather than where it is believed, and the thing that keeps this pin discriminating: reverse objectstack-ai#6245 and the case would pass while asserting nothing. ## Acceptance notes Observations from the surrounding code, noted and deliberately not acted on here: - `packages/spec/scripts/liveness/check-liveness.mts` builds its governance denominator from `listMetadataTypeSchemaTypes()` too, under the comment "i.e. exactly the set of authorable metadata types" — the same sentence objectstack-ai#17356 falsified for the reachability gate. `webhook` is patched in by hand (an `EXTRA_SCHEMAS` row plus `liveness/webhook.json`); `connector`, `sharing_rule` and `analytics_cube` are in neither `GOVERNED` nor `PENDING_GOVERNANCE`, so `report.ungoverned` cannot name them — they are not in its denominator. Filed separately rather than fixed here: whether those three should carry liveness ledgers is the governance question objectstack-ai#2657 leaves open, not a root-enumeration bug. - `packages/spec/src/ui/door-reachability.testkit.ts` runs the same enumeration and is **not** blind, because it pushes `ObjectStackSchema` as an extra root and reaches `connector` through `stack.connectors[]`. Corroborating, and the reason the fix here reads the kind map rather than adding the stack root: the kind map is the set of authoring doors, the stack schema is one of them. - The docblock on `listUnregisteredKindSchemaTypes()` still counts five kinds ("webhook / connector / sharing_rule / theme / analytics_cube"); the map holds four since objectstack-ai#10485 retired `theme`. One line of prose in a file this PR's declared surface marks read-only, so it is left alone rather than folded in. ## Scope Two files, both under `packages/spec/scripts/`. Neither is in the package's `files[]`, so nothing published moves — measured: `reachabilityRootTypes` has 0 hits across `dist`, `json-schema`, `liveness`, `prompts`, `api-surface`, `llms.txt`, `spec-changes.json`, `README.md` and `src/**/*.zod.ts`, against a positive control (`listUnregisteredKindSchemaTypes`) that has 7. Hence `skip-changeset`. --- ## The ruling this PR executes, and the shape it finally took Card objectstack-ai#17356's options went to the maintainer twice. The governing ruling is the second one, comment `5696910871` — **甲**: > Letter **A** lands FIRST, in its own anchor-only PR. Then PR objectstack-ai#18131 merges main. > **B** is refused: for `integration/DataSyncConfig:schedule` it would reverse the maintainer's 2026-09-10 retirement ruling recorded in the source. > **C** is filed: **objectstack-ai#18301** — check (c) gains a fourth proof so a guidance-route retirement on a reachable def proves itself; `data/Metric:filters` is its specimen. > **D** is refused. That supersedes the earlier batch objectstack-ai#135 reply (「135 同意」 on 「A 本 PR 内独立提交 + C 另立卡」), which had letter A as a second commit inside this PR. This body described that older shape until the merge landed; it is corrected here rather than left to become the squash commit message. **What actually happened, in order:** 1. **Letter A landed alone**, as PR objectstack-ai#18485 — one file, `packages/spec/authorable-surface.base.json`, +1024/-1056 — merged 2026-09-16T16:37:10Z as squash `fed4a15ab5`. The anchor advanced to `baseRev 85c6d76…`, 7804 keys. 2. **This PR then merged main** through `scripts/pm/os-regen-merge.sh` — never a bare `git merge`, because `.gitattributes:143` routes the anchor to `merge=os-regen`, a driver that exits 0 with no conflict markers while dropping one side. Merge commit `59c50319` (parents `dc98ee22a3` + `fed4a15ab5`), then the wrapper's step-3 commit `251d76a2`, which takes main's side of the anchor and changes nothing else. 3. **Letter C (objectstack-ai#18301) is untouched here** — no fourth proof is added to check (c) — and the objectstack-ai#16320 retirement of `integration/DataSyncConfig:schedule` is not reversed: the key is absent from the emitted surface and from the anchor. **The anchor is no longer part of this PR.** At head `251d76a2` the committed anchor is byte-identical to `origin/main`'s (`cmp` exit 0 at both `8cf527f8` and `fb6b2c36`), and `authorable-surface.base.json` does not appear in this PR's diff at all. The earlier branch-local advance to `b9598e9cab9d` (7772 keys) is history inside commit `dc98ee22a3`; the head carries main's `85c6d76e` (7804 keys). Anything this body said about "7772 vs 7772" belonged to that superseded shape. **The diff at head is exactly the two scripts** — `build-schemas.ts` +56/-4 and `build-schemas-check-mode.test.ts` +136/-2, 192+/6- — byte-identical to the original implementation diff `ceb66899^..c548dea`, with main having touched neither file between the original base and `fed4a15ab5`, nor since. Nothing was swallowed by the merge in either direction. **The seven reds are gone.** `Build Core`, `Dogfood Verify CLI`, `Dogfood Regression Gate` and its three shards, and `Temporal Conformance (live PG + MySQL)` were all `failure` on the pre-merge head `dc98ee22a3`, every one carrying the same annotation — `command (…/packages/spec) …/pnpm run build exited (1)` — from one root cause: this PR's own widened root set meeting a **stale committed anchor**, under which two keys retired on main after `53ef05744f37` surfaced as deletions this branch appeared to make. Advancing the anchor on main is what removed the premise. On head `251d76a2`, latest run per check NAME: **31 success, 4 skipped** (`Build Docs`, `Check Changeset`, `Console Pin Gate`, `Packed-tarball smoke (opt-in)` — no verdict, not green), 0 failure, 0 in progress. **`skip-changeset`, re-measured at head.** The two diff paths are under `packages/spec/scripts/` and are matched by none of the package's ten `files[]` entries; the anchor is not in the diff; `gen:schema` and `check:authorable-surface` on the pristine head both exit 0 and leave `git status --porcelain` empty. Nothing published moves, so no changeset is owed. **Contract review.** At-tier review on this exact head returned **PASS**; the record and the seat's adjudication of its escalated flags are in the PR thread. The `Clause-②: yes` declaration above is the conservative route into that review, not a claim that this diff publishes bytes — the review measured the bytes and found none. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…le def (objectstack-ai#18301) (objectstack-ai#18529) Fixes objectstack-ai#18301 Clause-②: no Executes the **C** half of the objectstack-ai#17356 ruling (batch objectstack-ai#135 item 3, maintainer 「135 同意」). A is already landed (PR objectstack-ai#18485 advanced the deletion-gate anchor); B and D were refused. This card adds a proof — it retires nothing, and it reverses nothing. > **Patch round.** The contract review of record (comment `5706880661`, served `CONTRACT_REVIEW_TIER`) returned **FAIL** on ① item 3: the proof's stated "the door is closed" condition described something the gate did not compute. This round replaces that condition, corrects every place the claim was made, and pins the case that was unpinned. Re-measuring the review's own sweep **falsified its latency finding** — see "The assumption that did not hold" below. ## What was wrong Check (c) of the `authorable-surface/` deletion gate (`packages/spec/scripts/build-schemas.ts`) admitted a deleted baseline line on three proofs: an aged-out `[RETIRED]` tombstone, an unreachable def, or a def the build no longer emits. A key retired the **strict-schema / guidance way** — deleted from the shape outright, its prescription moved into the closed shape's `guidance` table — never carries the `[RETIRED]` mark, because there is nothing left in the shape to mark. Proof 1 therefore could not apply to it **at any major**: not "has not aged yet" but "has no clock". On a reachable def that left the whole class with no proof shape at all. The class was invisible until now because proof 2 was answering for these defs — the BFS root set omitted the four unregistered kinds, so whole families read as unreachable and every deletion under them was waived as over-collection. objectstack-ai#18131 repaired the root set, and the repair is what exposes the gap. ## The assumption that did not hold The review swept for a def that could satisfy proof 4's conditions while silently STRIPPING the author's write, found none, and recorded the hole as latent. The dispatch asked for that to be re-measured. It was, **with the gate's own instrument** rather than by grep — a census pass over all 1525 emitted defs, running proof 4's declaration match and then asking each def what it does with the key. It is not latent: | def | artifact `additionalProperties` | matches one declaration by shape identity | reachable | writing `keyBy` | |---|---|---|---|---| | `shared/RateLimitConfig` | `false` | yes | `root-graph` | **parse SUCCEEDS, key dropped** | | `system/ServerRateLimitConfig` | `false` | yes (the SAME declaration) | `derived-clone` | refused, with the prescription | `ServerRateLimitConfigSchema` is declared `strictObject({… guidance: { keyBy, store } }, RateLimitConfigSchema.shape)` — built FROM the open schema's own shape object (`packages/spec/src/system/stack-server.zod.ts`, `packages/spec/src/shared/http.zod.ts`). So one declaration answers for two emitted defs, and **every fact the first cut of proof 4 read says they are the same def**. Two keys (`keyBy`, `store`) on a root-reachable def: had either baseline line been deleted, the shipped implementation would have waived it while an author who keeps writing the key has it silently dropped. That is the review's "strip-mode clone shares a strict shape" case in the spelling the tree actually holds — shape sharing in the other direction, which is why a sweep for `.strip()`, `z.object(X.shape)` and `strictObjectError()` found nothing. **No wrong verdict has shipped**: proof 4 is not on `main`, and neither key is a pending deletion. What changes is that the fix is now mandatory rather than prophylactic, and the fixture below is a real specimen rather than a synthetic one. ## What this adds **Proof 4.** A deleted baseline line is legitimate when, on a def that is emitted and reachable, **all three** of these hold in this build's own tree: 1. **the baseline entry was not `[RETIRED]`** — a guidance-route retirement deletes the key from the shape instead of leaving a `retiredKey()` in it, so it never earned the mark. This is a property of the class, not a guard bolted on, and it is what keeps proofs 1 and 4 disjoint. 2. **a `strictObject` declaration promises a prescription for the key** — the def resolves to exactly one `StrictObjectDeclaration` by shape identity, and that declaration's `guidance` names the exact key, or one of its `guidanceSets` **enumerates** it. This half says which text is owed. 3. **the def keeps that promise** — `safeParse` of that key against the schema `zodByDefKey` holds raises an `unrecognized_keys` issue naming it, and that issue's message carries the declared text **verbatim**. This half is the door. Condition 3 replaces the condition the review failed. Nothing else in the gate moves. ### Why the artifact read is gone rather than restated The failed version proved "the door is closed" by reading `additionalProperties === false` off the emitted JSON Schema. **This repo had already measured that this does not distinguish a closed door from a silent strip** and written it down: `build-schemas.ts` converts with the default `io: 'output'`, and in output mode zod emits `additionalProperties: false` for a `.strip()` object too — verified in `docs/audits/2026-07-unknown-key-strictness-ledger.md` by regenerating both ways to a byte-identical artifact. A condition that answers the same for both cases cannot be the one that excludes one of them, so it is removed, and the docblock and the author-facing remedy now say so in the gate's own words. The subtler half, which the review named and which the census above confirms: **shape identity is not a door test either.** `strictObjectError()` registers a declaration without closing the shape, `.strip()` and `z.object(Strict.shape)` clone a shape without its door, and `strictObject(opts, Open.shape)` — the live case — puts a closed declaration and an open def on the same shape entries. The identity match stays, because it is how the owed text is found; it is no longer asked to prove closure. ### Why the probe reads `unrecognized_keys`, and why it reads the message `unrecognized_keys` is the **only** issue code a `guidance` table is ever consulted from (`strictUnknownKeyError` returns undefined for every other code), and the prescription is appended to that message verbatim, one bullet per key. So the issue's presence is exactly "this def refused the write", and the declared text appearing in its message is exactly "the error map this def parses through is the one holding that table" — which shape identity alone cannot tell, since a clone can share a shape without sharing a map. No message WORDING is pinned by this: the needle is read out of the tree, from the very declaration the structural half matched, so a rewritten prescription moves both sides together. The alternative the review offered — reading `catchall` of type `never` off the instance — was measured to give identical verdicts on all four shapes tried (`strict`, `.strip()` clone, plain `z.object`, `catchall(z.string())`). It was not chosen because it proves a spelling of the door rather than the delivery of the prescription, and it would still have admitted a strict clone built without the declaration's error map. The other alternative — recording `strictObject()` and `strictObjectError()` distinctly in the registry — is a `packages/spec/src/shared/strict-object.ts` edit, outside this card's two files and across the clause-② path limb, and it would not have caught the live case above at all (both twins' declaration comes from the same `strictObject` call). ### A third verdict, and what it deliberately does not say A key a declaration names but the def does not answer for now gets its own violation line instead of the generic "was LIVE (never tombstoned)" — its `guidance` entry already exists, and what is missing is a door to deliver it through, so the generic verdict would send its reader to write something already written. That line states only **that** the prescription did not arrive, never **why**: on the shipped graph 7 of the 8 defs in that state are unions, where "the door is open" would be a guess this gate has not measured — the mistake the first cut made about `additionalProperties`, one layer down. ### Two narrowings, both deliberate, both fail-closed - **Exactly one matching declaration.** An empty shape is excluded outright — it matches every other empty shape. Where two declarations still answer, the lookup returns "no evidence" rather than unioning them. - **A `guidanceSets` RegExp does not count.** Only an enumerated `keys` list NAMES the key; a pattern claims a family whose members were never written down. ### Measured population — why this is a proof and not a blanket waiver Census over the shipped graph, run with the gate's own code (tree `944d773b8`; `packages/spec/src` is byte-identical at the head this PR now carries, `git diff --name-only` over that path returns 0 lines): | reading | value | |---|---| | emitted defs | 1525 | | defs whose emitted artifact carries `additionalProperties: false` | 1117 | | defs resolving to exactly one declaration that names an undeclared key | 258 | | keys those declarations promise | 779 | | keys the def actually delivers — what proof 4 admits | **770** | | keys promised and NOT delivered — what proof 4 refuses | **9** | Of the 9: 2 are the live case above; 7 are union defs the probe cannot drive to a single door, all of which the superseded artifact condition also excluded, so no verdict moves for them. `integration/DataSyncConfig` has no route at all (its shape is a plain `z.object` and nothing prescribes for `schedule`), so this proof cannot reach the 2026-09-10 ruling that withheld that tombstone. ## Evidence ### The pins (`build-schemas-check-mode.test.ts`) | fixture | expected | what it would catch | |---|---|---| | `data/Metric:filters` | admitted by **proof 4**, explicitly **not** proof 2 | a proof that never fires | | `data/Metric:zzNotPrescribed18301` | still refused, and NOT with the third verdict | a waiver keyed off the DEF instead of the KEY | | `integration/DataSyncConfig:schedule` | still refused | a silent reversal of the 2026-09-10 ruling | | `api/SessionResponse:zzOverCollected4650` | still waived by **proof 2**, in proof 2's words | proof 4 written as a widening of proof 2 | | `data/Object:compactLayout [RETIRED]` | falls to the tombstone chain, **not** proof 4 | the disjointness — it satisfies every other condition proof 4 tests | | `system/ServerRateLimitConfig:keyBy` | admitted by **proof 4** | — the lit half of the new pair | | `shared/RateLimitConfig:keyBy` | **REFUSED**, with the third verdict, and not waived by proof 2 either | **the review's finding**: one declaration, two defs, and a gate that reads the registry instead of the door admits the open one | The last two are ONE run and ONE declaration, which is what makes them a discriminator rather than two assertions. The `beforeAll` guard holds the tree fact they model in four loud halves: the two twins declare the same key SET, share every shape ENTRY by instance identity, the open twin ACCEPTS `keyBy` and the parsed output does not contain it, and the closed twin rejects it with a prescription bullet. If any half rots, the pin says so instead of going quietly green. Every negative assertion in the proof-4 cases was also corrected: they were written as `KEY — TOKEN` where the gate emits `KEY — def REACH; TOKEN`, so they could not have matched even on an admitted key. They now carry the `def .*` span and fail when they should. ### Ablations — both directions, on-disk proof, restored Both legs prove the mutation reached disk before any colour is read, and both restores are proved by `git hash-object` against the HEAD blob plus a whole-tree `git status --porcelain`. Each script arms a `trap` on EXIT, INT and TERM that restores the file from HEAD, against an absolute path resolved from `git rev-parse --show-toplevel`. **Ablation C — blind the door probe** (`delivers()` returns `true` unconditionally, which is the superseded implementation's behaviour for this def): - marker occurrences 0 to 1, blob `322938f2` to `682ce658` — the mutation is on disk. - run **RED**, and in the sharpest possible direction: `eager.status` came back **0**. With the door blinded the gate WAIVES `shared/RateLimitConfig:keyBy` and the whole run exits green — the hole, executed, not argued. The other two objectstack-ai#18301 cases stayed green, correctly: neither tests the door. - restored: blob back to `322938f2`, marker back to 0, `git diff HEAD` 0 bytes, `git status --porcelain` 0 lines. **Ablation D — deafen the door probe** (`delivers()` returns `false` unconditionally): - marker occurrences 0 to 1, blob `322938f2` to `3096b1af`. - run **RED**, 2 cases: both positive legs fall to the third verdict — `data/Metric:filters` and `system/ServerRateLimitConfig:keyBy` both printed `a \`strictObject\` declaration NAMES …, but writing it`. So the probe is load-bearing for the admissions too; proof 4 is not the declaration match wearing a new name. - restored: blob back to `322938f2`, marker back to 0, `git diff HEAD` 0 bytes, `git status --porcelain` 0 lines. The previous round's ablations A and B were run against the superseded implementation (their anchor, `prescribed?.has(leaf)`, no longer exists) and are **not** carried forward as evidence for this head. `scripts/ablation-dist-preflight.mjs` still reports `no dist/` for this package and is **NOT MEASURED**, not red, for the same reason as the previous round: the test spawns `tsx` over `scripts/build-schemas.ts` in a sandbox that SYMLINKS the real `packages/spec/src`, so nothing here resolves through `dist/`. The instrument that applies is the on-disk marker count plus the run's own colour, both recorded above. ## Runs Long runs went through `scripts/pm/os-verify-lock.sh`; exit codes were captured by redirect-then-`$?`, never through a pipe. `origin/main` was merged into this branch (`79a046f8c`) before this body was written, and every reading below is on the merged head. | command | verdict | |---|---| | `pnpm --filter @objectstack/spec run test:repo` | `VERDICT command-exit 0` — 31 files, 529 passed | | `pnpm --filter @objectstack/spec typecheck` | `VERDICT command-exit 0` (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`) | | `pnpm --filter @objectstack/spec run check:authorable-surface` | exit 0 | | `pnpm lint` (the repo-wide `eslint . --no-inline-config`) | exit 0 — the FULL run, not a narrowing, at `9e0324f80` | | `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` | 61 families derived ON THIS HEAD, not inherited | `pnpm lint` is normally CI's to run; it completed here, so the reading is the whole population eslint's own config selects rather than a subset — no narrowing claim is being made and none needs checking. All 61 derived gates were run and reconciled with `--ran`, each line carrying its exit code. 55 exit 0. Five exit **3 (PREREQUISITE NOT MET)** and are **NOT MEASURED** — each needs a built `dist/`, which this worktree has never had, and none can be moved by a diff confined to `packages/spec/scripts/**`: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt`. `pnpm check:pm-dispatch-gates` needed 807s and was recorded as `exit 124` on a first pass whose 600s wrapper fired; it was re-run without the cap and exits **0**. The record carries the real code, not the timeout. `packages/lint/scripts/check-reference-carrier-shape.mjs` is still present on this head and exits 0 — PR objectstack-ai#18503, which retires it, had not landed when this list was derived. The list was re-derived here rather than inherited from the dispatch, exactly because of that. ## Scope and publishing `packages/spec/scripts/**` matches none of the package's `files[]` entries (`dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `spec-changes.json`), and it is not a `tsup` entry — the only `scripts/` string in `packages/spec/tsup.config.ts` is a repo-root import, against a lit control of 22 `src/` occurrences. Nothing publishes, so `Clause-②: no` and `skip-changeset`. The diff is the two files the card fenced and no others: `git diff --name-only` against the merge base returns exactly those two. In particular the fix did **not** need `packages/spec/src/**` — the dispatch's stop condition on that point does not fire. ## Acceptance notes Out-of-scope observations, noted and deliberately **not** filed — none is a reproducible defect, a declared-contract breach, or a trap that makes an author write metadata the runtime rejects or silently drops: - **Superseded.** The previous round's note here claimed proof 4 "works around" the registry's door-blindness by reading `additionalProperties: false` off the emitted artifact. That was wrong, per the review and per this repo's own ledger, and the section above is what replaces it. Nothing about the registry is "worked around" now: closure is decided at the def, and the registry is asked only for the owed text. - `strictObject()` and `strictObjectError()` are indistinguishable in `strictObjectDeclarations()`, so the registry alone still cannot answer a door question. This proof no longer asks it one. Recording the two call shapes distinctly would let a future reader ask directly. Carrier: whoever next reads `strictObjectDeclarations()` for a door question. (`packages/spec/src/shared/strict-object.ts`) - 408 of the 1525 emitted defs do not carry `additionalProperties: false` on the emitted artifact. Per the review, that counts artifacts whose TOP-LEVEL field is not `false` — unions, loose objects, pipes — and is **not** the objectstack-ai#4001 ledger's strip-site population, which `check-strictness-ledger.mts` counts by AST. Carrier: the strictness-ledger worklist, which already owns that surface. - `scripts/ablation-dist-preflight.mjs` reports `no dist/` as a refusal, which is correct for a dist-mediated ablation and reads as an accusation for one that resolves through source. Carrier: none today — the script's header already prescribes the property-read alternative by hand. There is one observation this round declined to file and flags for the reviewing seat rather than burying: `shared/RateLimitConfig` is an **open** `z.object` whose shape is reused, closed, by `ServerRateLimitConfigSchema`, and the `guidance` entries for `keyBy` / `store` therefore prescribe to nobody on the open twin — an author writing `keyBy` on an API endpoint's `rateLimit` has it dropped in silence. That is objectstack-ai#4001's own failure mode on a live authorable surface, and it sits in `packages/spec/src/**`, outside this card's fence. It is a candidate class-(c) card for the triage seat, not a finding this PR may act on. --- _Generated by [Claude Code](https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho)_ --- ## Landing note (seat, 2026-09-17) Contract review at `CONTRACT_REVIEW_TIER` on head `9e0324f807`: **PASS** — record is comment `5707796462`. It supersedes the earlier **FAIL** (`5706880661`), which bound head `121465ba16` and does not bind this one. ⭐ **The re-review did not read this code, it ran it.** With no `node_modules` on the box it materialised `zod@4.4.3` and `esbuild` out of pnpm's content-addressed store, `git archive`d this head's `packages/spec/src` (archived tree hash verified equal to `git rev-parse 9e0324f:packages/spec/src`), bundled, and executed this head's own `computeGuidanceRoutes` — verbatim, `diff`-checked — against 11 synthetic door shapes and a full 1525-def census, with the OLD head's function alongside as the control. **The FAIL's one verdict-bearing item is closed, measured rather than argued:** `Strict` → prescribed, but `Strict.strip()`, `z.object(Strict.shape)`, `Strict.loose()`, an error-map object without `.strict()`, and `z.object(Strict.shape).strict()` without the map are **all refused**. Every one of those stripping forms also emits `additionalProperties: false` — which is the superseded condition's blindness demonstrated on the instance instead of quoted from the ledger. And the live twin executed both ways: `shared/RateLimitConfig:keyBy` reads `prescribed` through the OLD function (the hole, run) and `declared-but-silent` through this one. ###⚠️ Correction to this body The row 「defs resolving to exactly one declaration that names an undeclared key: **258**」 is **mislabelled**. That population measures **147**; 258 counts defs resolving to exactly one declaration *whether or not it names anything*. Corrected here because this repo squashes and the body becomes the permanent commit message. A second figure, the docblock's 「7 of the 8 defs in that state are unions」, is also wrong (9 keys on 4 defs, 3 unions) but lives **in code** — both are carried by **objectstack-ai#18579** rather than fixed in-branch, because a third push would move the head and void the review described above.⚠️ Neither figure moves a verdict or describes a safeguard, and the rationale they support (unions dominate the not-delivered set) survives the corrected arithmetic. ### Seat ruling on the process question the review referred here The review declined to rule on whether a dev may read a stop instruction by its stated rationale, and named it the seat's. **Ruling: the dev was right, and the dispatch order was at fault.** That order said 「if a live member exists, STOP AND REPORT — on the reading that it would mean a wrong verdict is shipping」. That bundles a **trigger** with a **rationale**. The dev measured the trigger TRUE, then measured the rationale FALSE (proof 4 is not on `main`; a `guidance`-only key is never in the shape, so it was never a baseline line and no deletion could ever put it to proof 4 — `keyBy`/`store` 0 in the baseline against a lit control of 1 for `enabled`), and disclosed both rather than quietly proceeding. Stopping there would have parked a proven-wrong proof in a draft and delayed a fix that had to land before this PR anyway. ⛔ This is **not** a general licence to reason past a fence. The correction belongs on the seat's side: a stop condition must be written as a **condition**, with its rationale separate and non-operative. The general rule stands — where a dev cannot measure the rationale false, the trigger governs and it stops. **Out of scope, correctly handed over rather than acted on:** the live trap the round found — `shared/RateLimitConfig` is an open `z.object` whose shape is reused *closed* by `ServerRateLimitConfig`, so an authored `keyBy` is dropped in silence — is filed as **objectstack-ai#18578**. It lives in `packages/spec/src/**`, outside this card's fence, and ⛔ was not folded in. **Pre-landing checks:** ① review PASS on record ✅ · ② `--pair 18529` exit 0; ⛔ no carriers hung (`Clause-②: no`, verified a true declaration against both limbs) ✅ · ③ re-taken at landing time ✅. Governed-surface predicate: **0 of 2 paths hit the register** ⇒ ordinary queue landing. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #17356— this PR carries letter A only: the anchor advance. The finding itself is fixed by PR #18131, which stays untouched this round; out of scope here: #17356 remains open, and #18301 (letter C) is serialized behind this.What is in the diff
Exactly one file —
packages/spec/authorable-surface.base.json, regenerated on this branch's own merge base with:⛔ No root-set widening. ⛔ No other file.
git diff --name-only origin/main...HEADprints one line.Why it lands here and not inside PR #18131
The authorable-key deletion gate (#4650) compares deletions against the baseline at the merge base with
origin/main— its own error text says so, andbuild-schemas.tsresolveSurfaceBase()reads the baseline keys out of git at that rev, never out of the anchor file in the tree under test:So advancing the anchor inside PR #18131 was a no-op against that gate: a commit in a PR cannot move the PR's own merge base. This PR's merge base is
mainitself, so once it lands, every later PR that mergesmaincarries the advanced anchor in its merge base.Measurements (this branch, UTC)
origin/main85c6d76ec42a879f219436eaa58bda06c68298b2(2026-09-16T15:19:10Z)baseRevbefore53ef05744f37789a3e2e2ee94325a616b706bea0— 7836 keysbaseRevafter85c6d76ec42a879f219436eaa58bda06c68298b2— 7804 keys⚓ authorable-surface.base.json refreshed to 85c6d76ec42a (7804 keys) — commit it, on its own.data/Metric:filtersin the new anchorintegration/DataSyncConfig:schedulein the new anchordata/Metric:keys in the same fileintegration/DataSyncConfig:keys in the same fileThe anchor was re-generated here, not cherry-picked from
dc98ee22a3: that commit's advance targetedb9598e9cab9d, andmainhas taken further landings since, so copying it would have pinned a stale rev. The numbers above are this branch's own, measured; they are not the ones the ruling quotes.--update-basewas not run mid-merge: this branch is a fresh branch offorigin/mainwith no merge in it, so the#5370refusal (build-schemas.tsrefuses--update-basemid-merge becausemerge-base(HEAD, origin/main)would re-anchor backwards) cannot apply and was not worked around.The rulings this executes
Ruling
5682578124(batch #135 item 3, maintainer 「135 同意」, 2026-09-15):Ruling comment
5696910871(class-1 self-adjudication, 2026-09-16T11:46Z), which re-carries letter A after the seat measured that the original carrier could not reach the gate:Letter B is not reversed by this PR:
integration/DataSyncConfig:schedulestays retired. Letter C (#18301) is not touched.The one premise this PR itself tests
The ruling records it as unmeasured, and this PR's first CI run is the measurement:
Read against the gate's source, the local expectation is narrower than an inference about proof 2: this branch emits the same authorable surface as its merge base (no schema and no root-set change), so on this PR the gate has no deletion to adjudicate at all. If CI disagrees, the first error line from the job log is reported and this is not patched around.
Acceptance notes
noted, not filed:the generator prints its anchor verdict as⚓ … — commit it, on its own, which is the advice this PR follows; nothing in the tooling enforces that the anchor advance travels alone, and the enforcement gap is what made the earlier in-PR carrier look valid. No card filed — it is an observation, not a reproducible defect, a contract violation, or an authoring trap.Generated by Claude Code