Skip to content

chore(spec): advance the authorable-surface deletion-gate anchor to main tip (#17356 letter A) - #18485

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-17356-anchor-advance-only
Sep 16, 2026
Merged

os-warren merged 1 commit into
mainfrom
claude/issue-17356-anchor-advance-only

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

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:

pnpm --filter @objectstack/spec gen:authorable-surface-base

⛔ No root-set widening. ⛔ No other file. git diff --name-only origin/main...HEAD prints 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, and build-schemas.ts resolveSurfaceBase() reads the baseline keys out of git at that rev, never out of the anchor file in the tree under test:

Deletions are therefore compared against the baseline at merge base 53ef057 with origin/main, which this commit cannot rewrite.

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 main itself, so once it lands, every later PR that merges main carries the advanced anchor in its merge base.

Measurements (this branch, UTC)

reading value
branch base / merge base with origin/main 85c6d76ec42a879f219436eaa58bda06c68298b2 (2026-09-16T15:19:10Z)
anchor baseRev before 53ef05744f37789a3e2e2ee94325a616b706bea0 — 7836 keys
anchor baseRev after 85c6d76ec42a879f219436eaa58bda06c68298b2 — 7804 keys
generator's own verdict line ⚓ authorable-surface.base.json refreshed to 85c6d76ec42a (7804 keys) — commit it, on its own.
data/Metric:filters in the new anchor 0
integration/DataSyncConfig:schedule in the new anchor 0
control — data/Metric: keys in the same file 6 (non-zero, so the zero readings above discriminate)
control — integration/DataSyncConfig: keys in the same file 8 (the def is still there; only the retired key left)
files changed 1

The anchor was re-generated here, not cherry-picked from dc98ee22a3: that commit's advance targeted b9598e9cab9d, and main has 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-base was not run mid-merge: this branch is a fresh branch off origin/main with no merge in it, so the #5370 refusal (build-schemas.ts refuses --update-base mid-merge because merge-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):

A — PR #18131 gains a second, separate commit that advances the committed anchor with pnpm --filter @objectstack/spec gen:authorable-surface-base, run in a full (non-shallow) clone; the PR body quotes this ruling (「135 同意」 on batch #135 item 3, presented as 「A 本 PR 内独立提交 + C 另立卡」). Advancing the anchor is the deliberate maintenance act the build's own output names; it forgives no future deletion.

B is refused: for integration/DataSyncConfig:schedule it would reverse the maintainer's 2026-09-10 retirement ruling recorded in the source.

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:

  1. Letter A lands first, in its own PR against main, containing exactly the anchor-advance commit lifted from dc98ee22a3 (authorable-surface.base.json only: 53ef05744f37 7836 keys → b9598e9cab9d 7772 keys, re-generated on the PR's own merge base if it has moved). ⛔ No root-set widening, ⛔ no other file. Its PR body quotes ruling 5682578124 and this comment.
  2. Once it is on main, PR fix(spec): the authorable-surface reachability roots include the unregistered kind schemas #18131 merges origin/main (via scripts/pm/os-regen-merge.sh, never a bare merge) so its merge base carries the new anchor; the two keys stop reading as deletions; acceptance 2 of the original dispatch is then measured on fix(spec): the authorable-surface reachability roots include the unregistered kind schemas #18131's next green run.
  3. Letter B stays refused (the spec: retire the seven cron-typed positions nothing reads — export schedules, ScheduleState.cronExpression, DataSyncConfig.schedule, CacheWarmup.schedule, backup/DR schedules — under ADR-0049 (#15954 ruling, option A per family) #16320 2026-09-10 retirement of integration/DataSyncConfig:schedule is not reversed). Letter C stays check (c) has no proof shape for a guidance-route retirement on a reachable def — add a fourth proof so UNKNOWN_KEY_GUIDANCE retirements prove themselves (batch #135 item 3, C) #18301 and is serialized behind this (same file).

Letter B is not reversed by this PR: integration/DataSyncConfig:schedule stays 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:

The seat's inference — «an anchor-only PR that does not widen the root set is green, because proof 2 still waives the two keys on that PR's merge base» — is unmeasured. The split PR's first CI run is the measurement. If it is red for the same reason, that is a genuine fork: stop, post the first error line, and this card returns to the decision box — ⛔ not patched around.

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

…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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface.base.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface.base.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1c331e94a257d8e66d0a4e1f447ec53e6c77a891

① Derived judgments

  1. Diff shape — one file, the anchor, nothing else. RIGHT. GET /pulls/18485/files = 1 file (packages/spec/authorable-surface.base.json, +1024/−1056); GET /compare/main...1c331e94 files = 1; local git diff --numstat 85c6d76e 1c331e94 = 1024 1056 on that path only. Every ± line in the diff is a key line except exactly two: - "baseRev": "53ef0574…" / + "baseRev": "85c6d76e…". No schema, no root set, no test, no changeset rides along.
  2. Regenerated on this branch's own merge base, not cherry-picked. RIGHT, re-derived byte-for-byte. Merge base per GET /compare/main...1c331e94 = 85c6d76ec42a879f219436eaa58bda06c68298b2 (the head commit's sole parent). In my own detached worktree at 85c6d76e (clean, no MERGE_HEAD) pnpm --filter @objectstack/spec gen:authorable-surface-base exited 0 printing authorable-surface.base.json refreshed to 85c6d76ec42a (7804 keys) — commit it, on its own, touched only the anchor, and produced sha256 01dc310f82cda77d913296bcb79a783b2c1e726af07f349fa40938c0bd830cc7 / git blob 49fd6af5da066d4a4ae1f7feae78a09f8d59aa8d — identical to the PR head's file (cmp exit 0; API raw content at head has the same sha256). Dark controls: differs from the base anchor (cmp exit 1) and from dc98ee22a3's anchor (cmp exit 1; that artifact is baseRev b9598e9c, 7772 keys, 307193 bytes vs head 308329). Head strictly contains dc98's 7772 keys plus 32 ui/ObjectGanttProps|ObjectMapProps|ObjectTreeProps:* keys that ui.json gained between b9598e9c and 85c6d76e (compare b9598e9...85c6d76: 16 commits, packages/spec/authorable-surface/ui.json touched) — a cherry-pick could not have produced them. The artifact's baseRev = 85c6d76e… = this branch's merge base.
  3. Key delta — every move accounted for. RIGHT. 7836 → 7804 (net −32). Computed by set difference: 1023 keys arrived, 1055 left; the line counts 1024/1056 are these plus the one baseRev line. Arrivals: 123 [RETIRED] tombstone lines + 900 live keys authored on main between 53ef0574 (2026-08-08) and 85c6d76e (ui 247, api 237, marketplace 189, data 107, system 47, automation 23, integration 16, kernel 15, security 12, shared 4, ai 3). Departures: 7 tombstone lines + 1048 live keys, of which 115 got their [RETIRED] twin in the same advance, 509 are the whole cloud/* namespace (0 cloud/* remain), 424 other defs main dropped, including data/Metric:filters and integration/DataSyncConfig:schedule (both 1 in base, 0 in head, 0 in live; controls data/Metric: 6, integration/DataSyncConfig: 8 in head). Legitimacy test, the gate's own criterion (compareAnchorKeys, both directions): head's 7804 keys == the aggregated authorable-surface/*.json shards at 85c6d76e — 0 shed, 0 invented, identical sorted order, 0 duplicates. So yes, keys were added, and every one of them is a key main's live surface holds at the merge base — the anchor moving forward, which is the ruled act. No invented key; the floor now equals main's surface at 85c6d76e.
  4. The untested inference (anchor-only advance does not red the deletion gate). HELD. Latest run per check name at head, 35 names: 0 failure/in-progress; the six required contexts are success (Build Core 15:34:45Z, Temporal Conformance 15:36:06Z, Dogfood Regression Gate 15:38:14Z, Lint & Repo Gates 15:37:55Z, TypeScript Type Check 15:40:50Z, Test Core 15:45:09Z). The deletion gate runs in Build Core (pnpm buildgen:schema) and in TypeScript Type Check (lint.yml:5165 check:authorable-surface) — both success. Locally on the PR-equivalent tree check:authorable-surface exited 0 with zero deletion lines: the branch emits the same surface as its merge base, so the gate had nothing to adjudicate.
  5. Two Check Changeset reds = label race. RIGHT, by timestamps. PR created 15:26:41Z; settle window = created_at + 120 s = 15:28:41Z (pr-automation.yml WINDOW_SECONDS: '120'). Failing runs: 104859357284 (PR Automation run 35115382968 created 15:26:45Z, completed 15:28:46Z) and 104859554808 (run 35115440177 created 15:27:16Z, 2 s after the needs:contract-review label at 15:27:14Z, completed 15:28:43Z). skip-changeset applied 15:30:46Z by os-warren (timeline labeled), 2m05s after the window closed. Third run 104860919012 (run 35115841580 created 15:30:49Z) concluded skipped via the job's payload fast path. Latest run per that check name is skipped, not failure; the check is outside the required six.
  6. skip-changeset correct. RIGHT, re-derived on the packed listing of a BUILT tree (tsup exit 0, 88 dist files; 1527 json-schema files). npm pack --dry-run --json = 1886 entries: authorable-surface.base.json 0, any authorable-surface path 0; lit controls dist/index.js 1, dist/* 88, json-schema/* 1527, src/**/*.zod.ts 201, api-surface/* 17, CHANGELOG.md 1; dark controls scripts/* 0, tsup.config.ts 0. Content ride-along: the new baseRev sha and the "baseRev" key appear in 0 packed files; lit control DataSyncConfigSchema lights 20, ObjectGanttProps 6. files[] = dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json — the anchor matches none. The tarball is byte-unchanged; nothing publishes.
  7. Independence from B, C and fix(spec): the authorable-surface reachability roots include the unregistered kind schemas #18131. RIGHT. fix(spec): the authorable-surface reachability roots include the unregistered kind schemas #18131 (head dc98ee22a3, draft) changes 3 files — its anchor (992/1056, baseRev b9598e9c) differs from this one, and its build-schemas.ts / build-schemas-check-mode.test.ts edits are absent here. Letter B not reversed: integration/DataSyncConfig:schedule 0 in head (control 8). Letter C: check (c) has no proof shape for a guidance-route retirement on a reachable def — add a fourth proof so UNKNOWN_KEY_GUIDANCE retirements prove themselves (batch #135 item 3, C) #18301 is an issue (pull_request absent), git ls-remote --heads has 0 issue-18301 branches, 14 open PRs contain no 18301 reference.

② Semver level

None — skip-changeset is the correct route; no changeset is owed. Rule: AGENTS.md § Post-Task Checklist step 3 (/home/user/objectstack/AGENTS.md, the paragraph beginning "Add a changeset for anything that publishes", ~L997–1001): the label "is for a diff that publishes nothing from any released package". Re-derivation on what ships: @objectstack/spec@17.4.0 npm pack --dry-run --json on the built PR-equivalent tree lists 1886 entries, the anchor 0 times, its content in 0 packed files (controls above). The Clause-②: yes question: the declaration exists — card claim 5699878219 (and 5696260242) spells Clause-②: yes, and the needs:contract-review label on the PR is read as yes by the level axis (scripts/check-changeset-no-major.mjs declarationFromPullRequest, carrier ⇒ yes; PR body carries 0 Clause-② lines). The same rule's mechanized form (judgeLevel) binds "at least one package whose packages/**/src/** the diff moves graded minor+"; this diff moves no src/** and no packed path, so grown = ∅, offenders = raised = [], verdict clean. The prose form binds "the widening it declares"; there is no widening — no public surface, accept-set or published byte changes. A minor changeset here would publish a CHANGELOG entry for a change no consumer can observe. node scripts/pm/check-clause2-carriers.mjs --pair 18485 exit 0 (both carriers labelled, declaration readable).

③ Boundary flags

  • Check Changeset stale reds — ANSWERED (item 5): documented race in pr-automation.yml header/L244–300; latest run skipped; not required. Nothing to file.
  • Ruling 5682578124's literal "full (non-shallow) clone" — ANSWERED: the dev's checkout and mine are shallow (--is-shallow-repository true, 1 shallow root, 8382 commits visible on origin/main), but the condition the wording guards (walkable ancestry 53ef057→85c6d76e for assertAnchorMovesForward) was proven positively: git merge-base --is-ancestor 53ef0574 85c6d76e exit 0 (positive is proof in a shallow clone per probeAncestry), GET /compare/53ef0574...85c6d76e status ahead, 5283 commits, and the generator wrote rather than refused. Ruling 5696910871 ("re-generated on the PR's own merge base if it has moved") is the operative text and is met.
  • Clause-②: yes on the card claim with no widening — ANSWERED (② above): a routing declaration (the seat's own words: "conservative … not a claim that the diff exports anything"); level axis clean; no minor owed. Nothing to file.
  • main has moved past the merge base — ANSWERED: GET /compare/85c6d76e...main at 16:04Z = 7 commits ahead (tip bf61f0a1, 22 files), 0 under packages/spec/authorable-surface/**, 0 under packages/spec/src/**, anchor untouched. The anchor still equals main's live surface at tip, so the queue rebuild sees 0 deletions / 0 additions; 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 owed.
  • PR body timestamp imprecision — ANSWERED, cosmetic: the body's "merge base 85c6d76 (2026-09-16T15:19:10Z)" is not the commit's date (committer/author 2026-09-16T14:18:02Z per git and API); the sha is right. Not load-bearing.
  • "commit it, on its own" is advice, not enforcement (dev's noted-not-filed) — CONCUR: observation, not a reproducible defect, contract violation or authoring trap. Nothing for the PM to file.
  • Governed surface / landing — the file is not a governed surface (Governed Surface Queue Guard success); PR is draft with no auto-merge and 0 reviews, awaiting this review as the claim states.

Implemented-by: claude/issue-17356-anchor-advance-only
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing provenance — PR #18485, card #17356 letter A, head 1c331e94a257d8e66d0a4e1f447ec53e6c77a891

domain:spec execution seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-16T16:15Z. Every reading is command output taken in this window, ⛔ not recall and ⛔ not any round's self-report.

Tier and independence

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 184850 of 1 path hits the register (5 surfaces)NOT governed, derived on the FINAL file list.

⚠️ ③ is read after the ready flip and after the carrier strip, because a strip re-triggers 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

@os-warren
os-warren marked this pull request as ready for review September 16, 2026 16:16
@os-warren
os-warren added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit fed4a15 Sep 16, 2026
46 of 48 checks passed
@os-warren
os-warren deleted the claude/issue-17356-anchor-advance-only branch September 16, 2026 16:37
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants