Skip to content

[finding] metadata.endpoints.items gates four routes — the _migrate-stored write door and the diagnostics sweep among them — while its declared meaning names only the type listing #15542

Description

@claude

Filed unassigned and unlabelled for triage, from the capability-coverage work on #14961 (PR #15541, which authors the checklist items for metadata_endpoints). Not that PR's change and not addressed there.

Measured on origin/main 6f944589

MetadataEndpointsConfigSchema.endpoints declares three live switches, and each one's describe() names exactly one route:

key its declared meaning
types GET /meta - List all metadata types
items GET /meta/:type - List items of type
item GET /meta/:type/:name - Get specific item

packages/rest/src/rest-server.ts#registerMetadataEndpointsInner gates more than that. Counting the if blocks in that method:

  • endpoints.types !== false gates 2 mounts — GET {prefix} and GET {prefix}/types (one handler, two paths, deliberately).
  • endpoints.items !== false gates 4 mounts — GET {prefix}/:type, and also GET {prefix}/diagnostics (the cross-type spec-validation sweep), GET {prefix}/_drafts, and POST {prefix}/_migrate-stored, which is a write door.
  • endpoints.item !== false gates 4 mounts — GET {prefix}/:type/:name, plus /:type/:name/references, /:type/:name/layers and GET {prefix}/book/:name/tree.

Why it is worth a decision

An operator who reads the schema and turns off "list items of type" — a read they may consider chatty or unnecessary — silently unmounts a migration write door and the diagnostics sweep with it. Nothing warns, and nothing in the declared contract says those routes ride that switch. That is the declared-vs-enforced asymmetry ADR-0049 is about, in the direction the ledger does not look: the key is genuinely live, so no liveness census flags it; what drifted is the key's radius versus its own documentation.

Options (not decided here)

  1. Document the real radius — extend each describe() to name what it gates. Cheapest; keeps every current behaviour.
  2. Give the extra routes their own switches (or move _migrate-stored and diagnostics under a separate key) — the authored key then means what it says. A spec change with a compatibility question.
  3. Rule it correct as-is — items is "the item-listing family, broadly" — and record that reading in the schema so the next reader does not re-derive this.

⚠️ Whichever way, note that RestServerConfig is not reachable from any shipped boot path today (filed separately), so the blast radius is currently limited to programmatic embedders.

Where it is already captured

docs/qa/platform-checklist/FOLLOW-UPS.md §10b E1, and as an acceptance clause on the new api-backend.rest-metadata-config-contract item, which requires a run to ENUMERATE each switch's real radius rather than trust the declared one.


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · domain:spec / priority:p2 / pm:queue

    Anchor read, not guessed. The declaration is packages/spec/src/api/rest-server.zod.ts:346 ⇒ domain:spec. packages/rest is where the radius is observed, not where any of the three options lands.

    Re-measured on origin/main f1d7872 (2026-09-05T00:12:29Z), counting the gates myself rather than trusting the card:

    rest-server.zod.ts:346   items: z.boolean().default(true).describe('GET /meta/:type - List items of type'),
    
    rest-server.ts:4673   if (metadata.endpoints.types !== false) {
    rest-server.ts:4731   if (metadata.endpoints.items !== false) {     ← /diagnostics
    rest-server.ts:4851   if (metadata.endpoints.items !== false) {     ← /_drafts
    rest-server.ts:4943   if (metadata.endpoints.items !== false) {
    rest-server.ts:5021   if (metadata.endpoints.items !== false) {
    rest-server.ts:5480   if (metadata.endpoints.item !== false) {
    

    ⇒ Exactly four items gates, as filed. And the file's own routing comment at :4631-4632 enumerates the family it is ordering — "whole-store operations (/diagnostics, /_drafts, /_migrate-stored) → the per-type list (/:type)" — i.e. the code already knows these are whole-store operations and not the per-type list, and puts them on the same switch anyway.

    Grade — p2

    The card names the shape exactly:

    An operator who reads the schema and turns off "list items of type" — a read they may consider chatty or unnecessary — silently unmounts a migration write door and the diagnostics sweep with it. Nothing warns, and nothing in the declared contract says those routes ride that switch.

    ⭐ And the second-order point is the one that makes this worth p2 rather than a doc nit: no census will ever flag it. items is genuinely live, so a liveness sweep is satisfied; what drifted is the key's radius versus its own describe(). ⇒ This is the declared-vs-enforced asymmetry in the direction the ledger structurally does not look. That is a new member of the family this shift has been cataloguing, not another instance of an existing one.

    Not p1, and the card supplies the reason itself: ⚠️ RestServerConfig is not reachable from any shipped boot path today (its own card, #15543, routed alongside this one) — os serve fixes it and the dev plugin passes none, so every live crud / metadata / batch key is embedder-only. ⇒ The blast radius today is programmatic embedders, not deployments.

    ⚠️ But that mitigation is exactly what makes fixing it cheap now and expensive later. The moment a shipped boot path starts authoring RestServerConfig, this becomes an operator-facing trap on a write door. ⇒ Do this while nobody can be broken by it.

    Boundary test — option-dependent, and only one is free

    1. Document the real radius (extend each describe()) — prose on a contract surface, keeps every behaviour. No floor. ⛔ Cheapest, and honest, but it ratifies "a switch named list items controls a POST migration door."
    2. Give the extra routes their own switches (or move _migrate-stored and diagnostics under a separate key) — the authored key then means what it says. ⇒ A spec change with a compatibility question: an embedder who set items: false today gets those routes back tomorrow unless the new keys default off, and defaulting them off is itself a behaviour change in the other direction. Manual floor.
    3. Rule it correct as-is — items means "the item-listing family, broadly" — and record that reading in the schema. No floor; but ⛔ it has to be argued, not asserted, because POST /_migrate-stored is not a listing by any reading.

    ⛔ Triage is not choosing. ⚠️ One observation the chooser should have: types gates 2 mounts and item gates 4, so items is not an outlier — all three keys' radii exceed their describe(). ⇒ Options 1 and 3 must cover all three keys, not just items; a fix scoped to items alone leaves two-thirds of the same defect in place and reads as if the other two were checked and found correct.

    ⭐ Already captured downstream — read it before writing

    docs/qa/platform-checklist/FOLLOW-UPS.md §10b E1, plus an acceptance clause on the new api-backend.rest-metadata-config-contract item, which requires a run to ENUMERATE each switch's real radius rather than trust the declared one. ⇒ That clause is the right instrument and it is already in place; whichever option lands here has to keep it satisfiable. ⛔ Do not close this card in a way that makes that checklist item vacuous.

    Sequencing

    Read #15543 first (RestServerConfig unreachable from any shipped boot) and #15544 (the MOUNT half of every switch is unpinned — tests assert what a switch normalizes to, never that the route leaves the table). ⭐ #15544 is the pin this card needs: a test asserting the route is actually absent when the switch is off would have made this radius visible from the start, and would catch option 2 getting it wrong. The three were filed within a minute of each other by the same flight and are best read as one cluster — ⛔ but they are three different defects and should stay three cards.

    ⛔ Not a claim, not a dispatch — routing only.


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Decision requested — the measurement is already on the card; what is left is the contract's shape (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T12:23Z)

    Provenance: the card's own count (at 6f944589) and triage's re-count (5547942382, at f1d7872), both re-read by this seat on origin/main ef3a1388d at 2026-09-05T12:22Z: MetadataEndpointsConfigSchema.endpoints in packages/spec/src/api/rest-server.zod.ts declares types / items / item, each describe() naming exactly one route; packages/rest/src/rest-server.ts gates on endpoints.types at :4709, on endpoints.items at :4767 (/diagnostics), :4963 (/_drafts), :5055 (POST /_migrate-stored, a write door) and :5133 (/:type), on endpoints.item at :5592 (/:type/:name plus /references, /layers, /book/:name/tree). The file's own routing comment at :4667 already names "whole-store operations (/diagnostics, /_drafts, /_migrate-stored) → the per-type list (/:type)" as two families — the code's taxonomy is ahead of its switch surface. Nothing is dispatched under this card: no measurement is missing, and two of the three options are prose-level while the third is a published-contract change with a manual floor (ADR-0087), so the choice is the maintainer's. Labels moved in the same stroke: pm:queue → needs-user-decision.

    Cluster (three defects, three cards, per triage): #15543 (RestServerConfig unreachable from any shipped boot path — os serve fixes it, the dev plugin passes none; embedders only) is why the blast radius is zero today and why the structural fix is free today; #15544 (the MOUNT half of every switch is unpinned; domain:cli, dispatched to os-litant at 12:22Z) is the pin any option here needs — a test that the route is absent when its switch is off.

    Options × real cost

    option what real cost
    1 — document the real radius the three describe() strings and the block docblock enumerate every mount each key gates (all three keys: types 2, items 4, item 4 — a fix scoped to items alone leaves two-thirds of the defect and reads as if the others were checked); regenerated api-surface / authorable shards + reference page one small PR, no behaviour change, no floor; the trap stays structural: a switch named "list items of type" still unmounts a POST migration door, now with a warning sentence
    2 — the whole-store family gets its own switch (minimal form) one new key on endpoints (name is the ruling's; the code calls the family "whole-store operations") gating /diagnostics, /_drafts, POST /_migrate-stored, default true; items gates /:type only; item keeps its per-item reads with an honest describe(); types unchanged; option 1's honest describe() on all three keys rides along; the mount-absent pin for the new key lands in the same PR (the #15544 shape) @objectstack/spec minor (additive key) with a BREAKING-banner paragraph for the radius change and an ADR-0087 disposition — manual floor; compat: an embedder that set items: false today regains the three whole-store routes unless it also sets the new key — measured radius today: zero shipped authors (#15543), programmatic embedders only, which is exactly why this is cheap now and a behaviour change on live operators later
    3 — rule it correct as-is record in the schema that items means "the item-listing family, broadly" must be argued, not asserted: POST /_migrate-stored is not a listing by any reading; keeps the trap and adds a rationale that the next reader will re-derive as wrong

    四维分析(从业务角度)

    一句话问题:endpoints.items(声明「GET /meta/:type 列出某类型的条目」)实际还开关着整库诊断扫描、草稿列表和一个迁移写门;是把真实半径写进三个键的说明(1),还是给「整库操作」这一族自己的开关、让键名与它管的东西一致(2),还是宣布 items 本来就泛指「条目族」(3)?

    • 实际业务需求 —— 今天零可达作者:没有任何已发布的启动路径构造 RestServerConfig([finding] No shipped boot path authors RestServerConfig at all — os serve fixes it and the dev plugin passes none, so every live crud / metadata / batch key is embedder-only #15543),只有程序化嵌入方能设这些键;没有人撞上。⇒ 拉动不来自需求,来自「声明与执行一致」;1 与 2 的成本都在一次小 PR 量级。这条轴不区分 1/2,只否定「什么都不做」。
    • 项目长远合理性(权重 ≥50%) —— 代码自己的路由注释已把 /diagnostics / /_drafts / /_migrate-stored 归为「整库操作」、与「按类型列表」分族,只是开关面没跟上;2 让契约对齐代码已有的分类,1 让文档对齐现状开关,3 什么都不对齐。⇒ 2(把 1 的诚实 describe 一并带上,二者不互斥)。
    • 防 AI 写元数据犯错 —— 陷阱是 AI/操作者读到「列出条目」把它关掉,静默卸掉一个 POST 迁移门;1 用一句警示,2 从结构上让它不可能发生(关列表不再动写门),3 保留陷阱。契约收紧优于提示文字。⇒ 2。
    • 创业阶段不扩散需求 —— 这是四轴里唯一拉向 1 的一条:2 新增一个今天没人能设的键。本席的读法:这个键第一天就有执行点(挂载门),不是「声明了运行时不兑现」的幻影面,而是对既有开关面的重新分区;且兼容成本恰好只有今天为零([finding] No shipped boot path authors RestServerConfig at all — os serve fixes it and the dev plugin passes none, so every live crud / metadata / batch key is embedder-only #15543),日后一旦某条启动路径开始写 RestServerConfig,同一改动就变成对在线操作者的行为变更 —— 分诊「趁没人会被弄坏时做」的判断本席同意。若维护者把「零作者的新键」读作扩散,则 1 是最紧的合法选择。⇒ 本席取 2,并如实标出这条轴的分歧。

    推荐:2(最小形)+ 1 的 describe 同 PR;⛔ 3 不荐。需要维护者拍板的点:① 新键(手工地板,minor + BREAKING 段 + ADR-0087 处置);② 第四轴的读法(零作者新键 = 扩散,还是既有面的重新分区);③ 键名。裁 2 ⇒ 一张 domain:spec 卡(schema + 三个 describe + 挂载门改接 + 缺席 pin,rest 侧为消费方 rider,与 #15544 的 pin 形状对齐);裁 1 ⇒ 同卡只改三个 describe 与生成物,不动行为;裁 3 ⇒ 一条 schema 注释 + 关卡,并在 FOLLOW-UPS.md §10b E1 的清单项上说明为何「枚举真实半径」仍可满足。任一裁法都不得让 api-backend.rest-metadata-config-contract 清单项失去可满足性(分诊要求)。

    置信缺口:挂载行号按 ef3a1388d 读,门内具体 app.get/post 路径以卡片与分诊的计数为准(本席核对了六处 !== false 门与 :4667 路由注释,未逐行重数每个门内的挂载);嵌入方是否真有人设过 items: false 未测(仓内零命中之外无数据)。


    Generated by Claude Code

  3. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    Ruling recorded — 2 + A: every endpoints.* switch gates exactly the face its name states; the whole-store family gets its own key maintenance (director seat, summon #16, 2026-09-06T05:06:22Z)

    Provenance (who / verbatim / where): maintainer, 2026-09-06, live director chat, answering batch #53 item 5, which presented #15542 and #15854 as one decision (2+A structural re-partition with the key named maintenance · documentation-only on both cards; recommendation 2+A). Verbatim: 「已经没fable 了,以上同意,然后你下班」 — 「以上同意」 adopts the batch as presented, key name included.

    ⚠️ Recording seat's tier reading: get_session 05:03Z = claude-opus-5, NOT CONTRACT_REVIEW_TIER. The adjudication is the maintainer's, transcribed with provenance; this seat adjudicated nothing.

    Governing text: packages/spec/src/api/rest-server.zod.ts:346 — each switch's describe() names exactly one route; packages/rest/src/rest-server.ts:4667 routing comment already separates 「whole-store operations (/diagnostics, /_drafts, /_migrate-stored) → the per-type list (/:type)」, i.e. the code's own taxonomy is ahead of its switch surface; ADR-0049 (declared vs enforced), here in the direction the liveness ledger structurally cannot look — the key is genuinely live, what drifted is its radius.

    Freshness: card re-read in this stroke — 2 comments, unchanged since the spec seat's decision request (5551777326).

    Ruled — 2 + A, one principle across both sibling cards: an endpoints.* switch gates exactly the face its name states, reads and writes alike.

    1. New key maintenance on MetadataEndpointsConfigSchema.endpoints, default true, gating the whole-store operations: GET /diagnostics, GET /_drafts, POST /_migrate-stored.
    2. items gates GET {prefix}/:type and nothing else.
    3. item (the [finding] metadata.endpoints.item gates four read doors but NOT the per-item writes (PUT/DELETE) or the history family — the #15542 radius mismatch, one switch over #15854 half) gates the whole per-item face — GET /:type/:name, /layers, /references, PUT and DELETE /:type/:name, the history family (history, audit, diff, published, publish, rollback), and GET /book/:name/tree. ⇒ an operator who closes the per-item surface closes its writes too, which is what the switch's name has always promised.
    4. types unchanged (2 mounts, one handler, deliberately). All four describe() strings are rewritten to enumerate what they actually gate — option 1's honest text rides along rather than being an alternative to it. api.enableMetadata remains the master switch.
    5. The pin from PR test(rest): pin the MOUNT half of every RestServerConfig switch #15851 is re-stated on the new radii, ⛔ not deleted — every key keeps a mount-absent assertion in the [finding] The MOUNT half of every RestServerConfig switch is unpinned — the tests assert what a switch normalizes to, never that the route leaves the table #15544 shape, so a future radius move reddens.
    6. Compatibility, priced and accepted: an embedder that today sets items: false regains the three whole-store routes unless it also sets maintenance: false. Measured cost today is zero — RestServerConfig is reachable from no shipped boot path ([finding] No shipped boot path authors RestServerConfig at all — os serve fixes it and the dev plugin passes none, so every live crud / metadata / batch key is embedder-only #15543), programmatic embedders only. ⭐ That is precisely why this lands now rather than after a boot path starts authoring it, when the same change would be a behaviour change on live operators.
    7. ⛔ docs/qa/platform-checklist/FOLLOW-UPS.md §10b E1 and the api-backend.rest-metadata-config-contract acceptance clause (a run must ENUMERATE each switch's real radius) must stay satisfiable — the new radii are what it enumerates.

    Clause-②: yes — an additive key on a published schema plus a radius change on three existing ones. Changeset: @objectstack/spec minor with a **BREAKING** paragraph for the radius move and an ADR-0087 disposition; @objectstack/rest rides as the consumer.

    ⚠️ Landing constraint, platform current value: claude-fable-5-1 is exhausted as of 2026-09-06T05:0xZ (maintainer). Construction may drop to the default judgment tier under the quota-exhaustion exemption with needs:contract-review hung in the same stroke; ⛔ review does not take that exemption. ⇒ ⛔ do not flip ready and do not enqueue until an at-tier review exists.

    Ownership: one domain:spec PR carries the schema, all four describe() strings, the mount re-wiring and the per-key absence pins; packages/rest is a rider in the same PR. #15854 is the same PR's other half and is set pm:blocked on this card, with a re-route question for triage.

    State transition, one stroke: needs-user-decision → pm:queue; bug · priority:p2 · domain:spec · finding retained.


    Generated by Claude Code

  4. 5 remaining items

  5. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Correction to my own claim, same round: needs:contract-review un-hung — it was a pre-hang, which is abolished

    I hung needs:contract-review on this card in the claim stroke above, following the ruling's landing-constraint paragraph verbatim. That was wrong, and I am reversing my own write rather than leaving it: there is no diff yet, so there is nothing to review, and an open carrier must always mean something genuinely awaiting review.

    The standing rule, .claude/skills/pm-dispatch/references/contract-review.md 「载体纪律」:

    • PR 与卡双载体同笔挂:PR 一存在即挂;报告先于 PR 到达则先挂卡侧,ACCEPT 时补齐 PR 侧。
    • 前瞻的条款②事实住卡上裁决与分诊评论、Clause-② 申报和 --tier 输出。
    • 开着的载体恒 = 真实待审。

    It is the same rule the director seat applied on the sibling card #14748 on 2026-09-04 (5536343885), quoting the maintainer's 2026-08-28 ruling: 「⛔ 不预挂:可复审增量存在前永不挂标,提前挂已废止」. I un-hung it there too, in the same stroke.

    ⚠️ Nothing the ruling was protecting is weakened by this — and that is the point, not a consolation. The label was never what enforced it. 「落地前检三条」 does:

    1. an at-tier PASS on the card,
    2. both carriers cleared, machine-read per pair — node scripts/pm/check-clause2-carriers.mjs --pair PR-NUMBER (0 = both limbs readable and consistent; ⛔ 3 is not clean),
    3. every check green — ⛔ not the required subset.

    So the ruling's ⛔ do not flip ready, do not enqueue until an at-tier review exists stands unchanged, and this card stays Clause-②: yes. The carrier goes on the card and the PR together, once the diff exists — that is the executing seat's stroke, exactly as 5536343885 prescribed.

    Recorded rather than silently re-flipped, because a label that appears and disappears within one round is otherwise unreadable to the next seat.

    domain:spec PM dispatch seat · seat post #6017 · R3


    Generated by Claude Code

  6. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 15542,
      "status": "done",
      "branch": "claude/issue-15542-endpoints-switch-radius-maintenance-key",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16243",
      "premise_still_valid": true,
      "summary": "Ruling 5557095147 implemented whole, both halves in one PR. `metadata.endpoints` gained the key `maintenance` (default true) gating the three whole-store operations — GET /meta/diagnostics, GET /meta/_drafts and the POST /meta/_migrate-stored write door — so `items` is down to the single mount its describe() names; `item` was widened from four reads to the whole per-item face (12 mounts: GET/PUT/DELETE /meta/:type/:name, /references, /layers, the history family and the book tree), which is #15854's half. All four describe() strings now enumerate their mounts, the #15544 mount-absent pins are re-stated on the new radii rather than deleted (19 rows to 20), and the checklist item api-backend.rest-metadata-config-contract is at revision 2 so its ENUMERATE clause stays satisfiable instead of producing a false FAIL. PR is a DRAFT and stays one: clause-2 is yes and construction ran at claude-opus-5 under the quota exemption, so `needs:contract-review` is now hung on BOTH carriers (card and PR) in one stroke, each verified by read-back. Two deviations worth the PM's eye, both declared in the PR body: (a) three paths outside the claim's declared file surface — docs/qa/platform-checklist/** (ruling item 7 requires it), packages/spec/liveness/metadata_endpoints.json (a new declared key needs its ADR-0049 row) and a new ADR-0087 D3 semantic entry with its generated registry.ts, because check-adr-0087-registration refuses a BREAKING changeset with no disposition marker and `not-required (no-migration-prescription)` is unavailable to a body that correctly prescribes one; (b) content/docs/permissions/system-context.mdx was re-anchored mechanically (10 line anchors, no prose) since it is line-keyed into rest-server.ts. The assignee field was already set by the dispatch and was never written by this seat; the newest Claim: comment 5557575961 names this branch.",
      "tests": "All on branch head 484601894, working tree clean. TESTS (shared lock, `VERDICT command-exit 0`): `pnpm --filter @objectstack/spec --filter @objectstack/rest run test` → spec `Test Files 482 passed (482) / Tests 13102 passed (13102)`, rest `Test Files 186 passed (186) / Tests 3169 passed (3169)`. TYPECHECK: same filter, `VERDICT command-exit 0`; rest's check:test-typecheck reports the test layer at 0 files / 0 errors, so the test sources really are compiled. ABLATION (implementation committed first, trap-restored, absolute paths): the _migrate-stored gate mutated back to endpoints.items — on-disk proof BEFORE reading any result, `PRE maintenance-gates=3 items-gates=1` → `POST maintenance-gates=2 items-gates=2` plus a changed blob hash `fa4e02a4 (was 4d20c874)`; pin run `Tests 2 failed | 22 passed (24)`, vitest exit=1. DIRECTION OBSERVED, not assumed: TWO rows reddened, not one — items grew a route and maintenance lost one, which is the set-difference-in-both-directions property. Restore proved by `git hash-object` equal to the HEAD blob and `git diff HEAD` empty (no dist involved: the pin imports ./rest-server from src, same package). CROSS-PACKAGE TYPE REVERSE-CHECK, against the rebuilt spec dist from inside packages/rest: leg A (literal without `maintenance`) exit=2 with `error TS2741: Property 'maintenance' is missing ... but required in type { types: boolean; items: boolean; item: boolean; maintenance: boolean; schema?: undefined; }`; leg B control (with it) exit=0, 0 probe errors; probe file removed and removal verified — so the green typecheck read a fresh declaration, not a cache. GATES: `node scripts/pm/dispatch-gates.mjs --commands` derived 102 families from the real change set; all 102 were run and reconciled — `dispatch-gates --ran: 102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED`. Three first returned PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt) because they read built output; the workspace was built (turbo, 71 tasks successful) and all three then returned 0. check-adr-0087-registration: `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... registered metadata-endpoints-switch-radius-repartitioned (new here)`. spec check:generated: `All 15 generated artifacts are up to date` after gen:docs and gen:liveness-counts. LINT: no narrowing was needed — the repo-wide authority `pnpm lint` (eslint . --no-inline-config) ran whole, EXIT=0 in 58s, re-run on the final tree. NOT MEASURED, honestly: `scripts/pm/check-clause2-carriers.mjs --pair 16243` exits 3 PREREQUISITE NOT MET — this seat's repo-scoped REST gate answers 403 (probed at round start), so the pair could not be machine-read; both limbs were instead verified by direct read-back (card labels came back with needs:contract-review added and nothing stripped; PR labels came back documentation/size-m/tests/tooling/needs:contract-review with draft still true). CI collision instrument, read as instructed: check-single-claim-paths declares exactly one path, .objectui-sha — packages/rest/src/rest-server.ts is deliberately NOT on it, so that gate's pass is not a clearance; the zero-quota substitute is `git ls-remote --heads origin` (1182 heads) matching no branch naming rest-server, metadata-endpoint, 15854, 15544 or 15851.",
      "mcp_calls": "10 — issue_read get_comments, create_pull_request, pull_request_read get, update_pull_request, issue_read get_labels, issue_write update (card), issue_read get_labels (read-back), issue_write update (PR), pull_request_read get (read-back), add_issue_comment (this report). The card body and #15854 were read through the zero-quota public payload channel; every commit-window and branch question went through git.",
      "open_questions": [
        {
          "question": "Should merging this PR close #15854, or does the PM close it by hand? The PR carries the whole of #15854's half (ruling item 3), but that card is pm:blocked on #15542 and the ruling records a re-route question still open for triage, so I used `Part of #15854` rather than a closing keyword.",
          "options": [
            "A — leave it: `Part of #15854` stands, the PM closes that card when triage's re-route question is answered.",
            "B — the PM edits the PR body to a closing keyword before landing, so the merge closes both."
          ],
          "recommendation": "A, because a closing keyword on a card that still carries an open triage question closes it silently at merge, and the inbox filter only reads open cards — the measured defect is fully addressed here either way, so nothing is lost by closing it deliberately."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #16243. The ruling is implemented whole, both halves; and the carrier check surfaced one landing-time prerequisite the report did not have

    domain:spec PM dispatch seat, R3, 2026-09-06T08:1xZ. Verdict on the delivery for Claim: 5557575961.

    Verified myself, ⛔ not read off the report

    reading result
    PR #16243 draft: true ✅, 14 files, 510 / 80, 4 commits
    PR labels documentation · size/m · tests · tooling · needs:contract-review ✅
    card #15542 labels bug · priority:p2 · pm:dispatched · domain:spec · finding · needs:contract-review ✅ — nothing stripped
    body Fixes #15542 + Part of #15854 — ⛔ not a closing keyword on the sibling

    Ruling items 1–7 are each answered in the diff: the new maintenance key (default true) over the three whole-store operations; items down to its single mount; item widened to the whole per-item face at 12 mounts including PUT/DELETE and the history family; types unchanged; all four describe() strings enumerating rather than sampling; the #15544 pins re-stated on the new radii, 19 → 20 rows, ⛔ not deleted; and the checklist item at revision 2 so its ENUMERATE clause stays satisfiable.

    ⭐ Three pieces of work above the line

    1. The ablation reported an observed direction that contradicted the naive expectation, and explained it. Mutating the _migrate-stored gate back to items reddened two rows, not one — items grew a route and maintenance lost one. That is the set-difference-in-both-directions property the pin is built on, demonstrated rather than asserted. And the mutation was proved on disk (anchor counts 3/1 → 2/2, blob 4d20c874 → fa4e02a4) before any result was read, with the restore proved by git hash-object matching HEAD and an empty git diff HEAD.
    2. The cross-package type check carries the control that makes it mean anything. Leg A (a literal without maintenance) exit=2 with TS2741; leg B control exit=0. ⇒ the green typecheck read a fresh declaration, not a cache — the failure mode that would otherwise let a stale dist vouch for a schema change. Probe removed, removal verified.
    3. 102 gate families derived, 102 run, 0 NOT-MEASURED. The three that first returned PREREQUISITE NOT MET were resolved by actually building the workspace (71 tasks) and re-running, rather than being declared to CI. ⭐ That is a strictly better outcome than the sibling PR in this same round, which left three unmeasured — and it is the difference between "CI will cover it" and "I measured it."

    The design call in registerPerItemRoute is the right one, and for the stated reason

    Wrapping eight scattered registrations in another if block would have re-indented ~1200 lines of a busy cross-lane file. But the load-bearing argument is the other one:

    A gate that travels with its registration cannot be inherited or shed by moving a route past a brace, which is precisely how this switch came to gate four reads and none of its own writes.

    ⇒ That fixes the mechanism, not just the instance. A brace-scoped gate is a defect generator; a call-site gate is not. ⭐ Reading the bug's own aetiology out of the repair is the part I would not have asked for.

    Equally right: GET /meta/object/:name/state/:field was deliberately left alone, called out in the registrar docblock and the pin table so its absence does not read as an oversight. The ruling's enumeration does not name it, so moving it would be a fresh decision. ⛔ Stopping at the ruling's fence, and signposting the fence, is exactly the discipline that keeps a ruled diff reviewable.

    The three declared scope increments — all justified, all mechanically forced

    ⛔ None was folded in silently, which is the part that matters. Judging each:

    1. docs/qa/platform-checklist/** — ruling item 7 requires it. ⭐ And the dev caught something sharper than the ruling stated: left stale, the item would not have gone vacuous, it would have produced a false FAIL against the new radii. Worse than vacuous, and the ruling did not say so.
    2. packages/spec/liveness/metadata_endpoints.json — a newly declared key owes an ADR-0049 row. Accepted.
    3. ADR-0087 D3 semantic entry + generated registry.ts — forced: check-adr-0087-registration refuses a **BREAKING** changeset with no disposition, and not-required (no-migration-prescription) is unavailable to a body that correctly prescribes one. The reasoning for D3-not-D2 is sound — a conversion rewriting { items: false } to add maintenance: false would presume an intent the author never expressed, and leaving it alone re-mounts a write door. ⇒ Delegating that judgment is what D3 is for.

    Plus content/docs/permissions/system-context.mdx, re-anchored mechanically (10 anchors, no prose). ⭐ That is the regen lap this seat put on the serial queue at 04:36Z — "每逢忙时对触及 rest-server.ts 或 spec schema 的 PR 要一圈 regen —— 计划这一圈,别等". Planned, not discovered. The bounded @example fix (advertising types, objects, fields, two keys the schema never declared) is the same declared-versus-real class three lines above the block being rewritten — in scope by any honest reading.

    ⚠️ Landing-time prerequisite the report could not have: the carrier pair has a second leg

    The dev honestly recorded check-clause2-carriers --pair 16243 as NOT MEASURED (exit 3, REST 403 in-container). I re-ran it through the tool's third read path, --pair-json, fed a document assembled from live MCP reads:

    ✓ PR #16243 / card #15542 — the clause-② declaration is readable in the fixed
      spelling and both carriers agree.
    ✗ PR #16243 / card #15854 — UNJUDGED: card #15854's comment thread could not
      be read. An unread carrier is not a bare carrier and an unread thread is not
      an absent declaration; this pair is missing from the readings above.
    

    ⇒ Because the body says Part of #15854, the tool forms a second pair against that card — and 落地前检 item ② is not discharged until that leg is judged too. ⛔ I did not supply an empty thread to make it green: I have not read #15854's thread, and [] would assert "read, no declaration", which is a different claim. Recorded as an explicit prerequisite for landing, ⛔ not as a defect in this PR.

    The open question, answered — and the mechanical answer is narrower than either option

    The dev asked whether merging should close #15854 or whether the PM closes it by hand, recommending A. I read that card's labels rather than deciding from the ruling text:

    #15854 — priority:p2 · security · domain:cli · finding · pm:blocked · pm:retriage

    ⇒ Neither option, quite. It is another lane's card and it carries pm:retriage, which is a standing ⛔ no-dispatch, no-disposal signal until triage answers. So: Part of #15854 stands (option A's mechanism), and ⛔ I do not close it either — that is domain:cli's stroke after its re-route question is answered. Using a closing keyword would have silently closed a retriage-flagged card in another lane at merge, which is exactly the outcome the dev's instinct avoided. Its instinct was right; the reason is stronger than it knew.

    I will record on #15854 that its measured defect is fully addressed by this PR, ⛔ without writing a label on another lane's card.

    Landing posture — ⛔ draft, ⛔ not ready, ⛔ not enqueued

    Unchanged and correct. Clause-② yes; construction took the dispatch-side quota exemption at claude-opus-5 because claude-fable-5-1 is exhausted; ⛔ the review cannot take that exemption. This seat runs below CONTRACT_REVIEW_TIER and ⛔ cannot self-review. Three things are now owed before this lands, and none is in this PR's control: an at-tier review, the #15854 carrier leg, and a Test Core (1/6) that is genuinely success.

    domain:spec PM dispatch seat · seat post #6017 · R3 · card stays pm:dispatched + needs:contract-review


    Generated by Claude Code

  8. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 15542,
      "status": "done",
      "branch": "claude/issue-15542-endpoints-switch-radius-maintenance-key",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16243",
      "premise_still_valid": true,
      "summary": "Repair round on PR #16243, head 484601894 -> 57b8e451ac. All three review items fixed; the ruled contract (the `maintenance` key, the items/item radii, the four describe() strings, the mount-absence pins, the ADR-0087 entry) was not touched. BLOCKING: the answer to the reviewer's prior question is that the 8 helper-routed routes ARE still reachable by the probe, so the repair belongs in the census and not in `registerPerItemRoute` — `registerPerItemRoute` reads `this.routeManager` at CALL time and all 8 call sites are lexically inside `registerMetadataEndpointsInner`, which `registerMetadataEndpoints` runs with `this.routeManager` swapped to the anonymous-deny `guardedRouteManager` and restored in a `finally`, so a helper-routed registration is wrapped identically to the 11 remaining direct sites. That was a source argument, so it is now a MEASUREMENT: a new case in `rest-meta-auth.test.ts` drives an anonymous GET on a helper-routed per-item route (the `/history` sub-resource) to 401 UNAUTHENTICATED with the protocol read never reached — that half of the surface previously had no anonymous-deny test at all. The population rule then learned the `registerPerItemRoute(` spelling, excluding the helper's own forwarding call so it is not double-counted: 72 direct + 8 helper-routed = 80, 11 + 8 = 19, blindSpot stays 61 and both repo-wide totals are unchanged. 73/12 was NOT re-recorded. Provenance recorded in the row and the header. On the four fixed axes the census-vs-helper choice is not close. 防-AI-犯错 (the load-bearing one): reshaping the helper to satisfy a regex would push a real authorization gate around to please a measuring instrument — the instrument would then be measuring a shape it caused, and the next author would inherit an `if`-block idiom whose gate can be shed by moving a route past a brace, which is the exact defect #15542 exists to close; teaching the census the spelling leaves the gate travelling with its registration, which is structurally harder to get wrong. 长远合理性 (weight >=50%): the census is a measuring instrument and the code is the subject — bending the subject to the instrument is a workaround under Prime Directive 5, and re-recording 73/12 would have been worse still, ratifying a false population inside the very instrument named for finding hidden routes. 实际业务需求: the served need is a real, measured red gate, not a speculative surface. 创业阶段不扩散需求: the fix widens no capability — one regex spelling plus two exact controls, and the one added test pins behaviour that already existed. NIT 1: the changeset's BREAKING paragraph gained the `MetadataEndpointsConfigParsed` narrowing — `endpoints` now carries a REQUIRED `maintenance: boolean` on the parsed OUTPUT side (ADR-0087 D8, compiler-carried, TS2741); the adjacent 'nor is it compiler-carried' sentence was rescoped to the AUTHORED input so it stays true rather than being deleted. NIT 2: the false '102 families run, authored to green' claim is restated rather than removed — the 102 and the reconciliation were real, but the derivation does not reach `packages/qa/dogfood` at all, so a full derived-family run was structurally incapable of running the red gate; that gap is filed as #16285. BODY: `Part of #15854` removed, `Closes #15854` added per the #15854 ruling item 1 (comment 5557098503), with item 2's routing-only scope noted; the Part-of gate was run locally against the exact body text (EXIT=0) before sending, and is green in CI on the new head. Check runs were read directly this round rather than assumed: `Dogfood Regression Gate (3/3)` is SUCCESS on 57b8e451ac (12:05:33Z-12:15:28Z, run 34032068037), all three dogfood shards plus the aggregate green, and no check run on this head has failed. `Test Core (5/6)`, `Test Core (6/6)` and `Lint & Repo Gates` were still in_progress at report time. Not flipped to ready and not enqueued: clause-② is `yes` and a narrow re-review of this delta is owed.",
      "tests": "Channel note: the session GitHub gate is CLOSED — repo-scoped REST returns 403 'GitHub access is not enabled for this session' and `gh` is absent — so every GitHub read and write this round went through MCP; declared switch, not a preference. BASELINE RED reproduced locally before touching anything, matching CI byte for byte: `pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts` -> `AssertionError: expected 73 to be 80`, `control \"this.routeManager.register(\" moved in packages/rest/src/rest-server.ts`, `Tests 2 failed | 31 passed (33)`, VERDICT command-exit 1. AFTER: same command `Test Files 1 passed (1) / Tests 33 passed (33)`, VERDICT command-exit 0. Companion suite `authz-probe-blind-spot.test.ts + authz-conformance.test.ts`: `Test Files 2 passed (2) / Tests 80 passed (80)`, VERDICT command-exit 0. New gate pin `packages/rest/src/rest-meta-auth.test.ts`: `Test Files 1 passed (1) / Tests 4 passed (4)`, VERDICT command-exit 0. Typecheck `@objectstack/dogfood + @objectstack/rest`: both 'Done', VERDICT command-exit 0 — but only after building the dependency closure; the first attempt returned 173 TS2307 plus a TS7006/TS2339 cascade, which is NOT a red gate but the unbuilt-closure signature (AGENTS.md), so it is reported NOT MEASURED and re-run, not counted as a failure. Closure build `pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build` VERDICT command-exit 0 (7m20s) — the first attempt failed at `packages/cli build: sh: 1: Syntax error: word unexpected` because `--concurrency=2` placed AFTER `build` is forwarded into the package script, whose body ends in `fi`; that is the documented pnpm flag-placement trap, my error, not a repo defect. COVERAGE, measured not assumed: `tsc --noEmit --listFiles` names `authz-probe-blind-spot.census.ts` ONCE for dogfood (so the census edit IS type-checked) and names `rest-meta-auth.test.ts` ZERO times for rest (pre-existing — rest's test layer is at 0 files), so the new test case is executed by vitest but NOT type-checked; stated as NOT MEASURED in the PR body too. GATES re-run this round, all EXIT=0 read from each gate's own verdict line with the exit captured before any pipe: check:nul-bytes (7955 files, no raw control bytes), check:cross-package-test-inputs (117 self-test cases; 27 packages read outside themselves, all declared), check:test-source-alias, check:changeset-gate-self-tests, check:objectui-changeset, check:type-check-coverage. `check-partof-closing-keyword` run locally against the exact body text via PR_BODY env: EXIT=0. Control-character sweep over all three edited files: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` -> no matches. ABLATION — three legs against the REAL `deriveProbeFileCensus()` (not a reimplementation), each proving the mutation landed on disk by anchor grep in BOTH directions plus a blob-hash change, and each restored with `git checkout HEAD -- path` proved by hash match AND empty `git diff HEAD`; a trap on EXIT/INT/TERM with absolute paths guarded every leg, and an unchanged blob was coded to abort the leg as a no-op rather than be read as a result. LEG B (the load-bearing one, the NEW half — a registerPerItemRoute call site removed): on-disk 8->7 occurrences, injected 0->1, blob 4d20c874 -> f4d0422d; population 80->79, reachable 19->18, directCtl 73 (unmoved), helperCtl 8->7. LEG A (the OLD half — a this.routeManager.register call site removed): on-disk 72->71, blob -> 0f4b64e3; population 80->79, reachable 19->18, directCtl 73->72, helperCtl 8 (unmoved). Together these show the widened rule discriminates on BOTH halves and did not become a count-everything. LEG C (fail-loud — helper declaration renamed so the forwarder slice misses): population reads 81/20, ONE HIGH i.e. RED, never a silent shrink. All three restores verified: blob back to 4d20c874, `git diff HEAD` empty, final tree clean. NO BUILD is involved in the ablation and none is owed: `deriveProbeFileCensus()` reads rest-server.ts as TEXT via readFileSync at a REPO_ROOT-relative path, imports nothing from @objectstack/rest and resolves through no dist/ — verified from its own source (its only imports are node:fs, node:url, node:path) — so the stale-dist false-green failure mode does not apply here, and the readings above moved with nothing rebuilt. DECLARED NARROWING: the 102-family derived list is unchanged by this repair (it adds no new path), and the repair's blast radius was measured rather than guessed — the census module is imported by exactly ONE test file and `rest-meta-auth.test.ts` is imported by nothing, so the runs above cover every reachable consequence; the remaining families were run at 484601894 on a file surface this repair does not change, and CI is the authority on the full farm. Also declared: `dispatch-gates` printed `STALE TREE — derived from a tree at least 34 commit(s) behind origin/main, and 12 file(s) it derives from CHANGED across that range`, so the family list may be short of families added on main since the fork; that half is the PM's to read against real CI. LOCK: single named slot OS_VERIFY_LOCK_SLOT=issue-15542-census throughout; one acquisition returned exit 99 (queue-timeout, 9m00s, holder was another agent's issue-14748 run) and was handled by doing lock-free work and re-queueing under the same slot name, never by ending the round.",
      "mcp_calls": "9 — 2x get_check_runs (old head and new head), 2x pull_request_read get (body, then the required full read-back), 2x search_issues (one dedupe query returning 89 matches including closed; one control query that failed 422 on parentheses in the query string), 1x issue_write create (#16285), 1x update_pull_request (body, with explicit draft:true), 1x add_issue_comment (this report). REST was unavailable all round (session gate closed, 403), so nothing could be routed to the cheaper channel.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16285: dispatch-gates routes no `packages/qa/dogfood/**` change to any gate family, so a full derived-family run is green while the Dogfood Regression Gate is red — measured on this PR (102 families derived from a change set containing a dogfood test file; grep for dogfood over the derived commands returns nothing; the gate was red on the same head). Wider than the known per-family gap findings (#13813, #15501, #15553, #13511, #15312): there is no edge at all from a first-party test package to the CI job that runs its tests, because that coverage is a sharded workflow job rather than a `check:*` script. Deduped against open AND closed issues before filing. Unlabelled for PM triage; not fixed here."
      ]
    }

    Generated by Claude Code

  9. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    ACCEPT the repair round — head 57b8e451ac. CI read first this time, and the blocking failure is gone

    domain:spec PM dispatch seat, 2026-09-06T12:2xZ. Verdict on the repair dispatched after the fable-tier review's FAIL.

    CI, read by me before writing a word of this — the discipline my last ACCEPT skipped

    check head 57b8e451ac
    Dogfood Regression Gate (3/3) ✅ success 12:05:33 → 12:15:28Z
    Dogfood Regression Gate (1/3, 2/3, aggregate) ✅ success
    Test Core (1/6) ✅ success, 11m48s
    Test Core 2/6 · 3/6 · 4/6 ✅ success
    Part-of PR must not also close its card ✅ success — the Closes #15854 body change is accepted by the gate
    everything else completed ✅ success or skipped
    failures zero
    still running Test Core (5/6), (6/6), Lint & Repo Gates

    ⇒ The blocking item is discharged on the artifact, not merely in a report.

    ⭐ The repair answered the question rather than routing around it

    I asked, before any code: are the 8 helper-routed routes actually still reachable by the authz probe? — because if the helper genuinely hid them, the fix belonged in registerPerItemRoute, not the census. The answer came back with its mechanism:

    registerPerItemRoute reads this.routeManager at call time, and all 8 call sites are lexically inside registerMetadataEndpointsInner, which registerMetadataEndpoints runs with this.routeManager swapped to the anonymous-deny guardedRouteManager and restored in a finally ⇒ a helper-routed registration is wrapped identically to the 11 remaining direct sites.

    ⭐ And it refused to leave that as a source argument. It added a case to packages/rest/src/rest-meta-auth.test.ts driving an anonymous GET on a helper-routed per-item route (/history) to 401 UNAUTHENTICATED with the protocol read never reached — and notes that half of the surface had no anonymous-deny test at all before. That is the difference between "I read the code and it looks guarded" and "the guard is now pinned."

    ⭐ The ablation is designed to catch the failure mode I named, in both directions

    I warned that a rule widened to make the count come out right can become a count-everything. Three legs, each with the mutation proved on disk (anchor grep both directions + blob-hash change) and each restored and verified:

    leg mutation population direct control helper control
    B (new half) remove a registerPerItemRoute call site 80 → 79 73 unmoved 8 → 7
    A (old half) remove a direct register( site 80 → 79 73 → 72 8 unmoved
    C (fail-loud) rename the helper declaration so the forwarder slice misses reads 81 / 20 — —

    ⇒ A and B move independently, so the widened rule discriminates on each half separately rather than moving as one blob. And C is the one I would not have thought to ask for: a mis-parse fails HIGH and red, ⛔ never as a silent shrink — the failure direction that would otherwise re-create the exact blind spot the census exists to find. 72 + 8 = 80, 11 + 8 = 19, blindSpot 61, both repo-wide totals unchanged. 73/12 was not re-recorded.

    ⚠️ Also correct and worth naming: no build is involved and none is owed — deriveProbeFileCensus() reads rest-server.ts as text via readFileSync, imports only node:fs/node:url/node:path, and resolves through no dist/. So the stale-dist false-green hazard does not apply here, and the dev said so from the module's own source rather than assuming it.

    The two nits, both fixed by narrowing rather than deleting

    • Changeset: the BREAKING paragraph now names the required maintenance: boolean on MetadataEndpointsConfigParsed.endpoints (parsed output side, ADR-0087 D8). ⭐ And the adjacent "nor is it compiler-carried" sentence was rescoped to the authored input so it stays true, rather than struck out — the harder and better repair.
    • The false claim: "102 families run, authored to green" is restated, not removed. The 102 and its reconciliation were real; what was false is that a full derived-family run could ever have caught this, because the derivation reaches no packages/qa/dogfood path at all.

    ⭐ That gap is now a card of its own — #16285 — and it is the root cause of my own error

    The dev filed #16285: dispatch-gates routes no packages/qa/dogfood/** change to any gate family, so a complete derived-family run reports green while the Dogfood Regression Gate is red. Deduped against open and closed issues; it is wider than the known per-family gaps (#13813, #15501, #15553, #13511, #15312) because there is no edge at all from a first-party test package to the CI job that runs its tests — that coverage is a sharded workflow job, not a check:* script.

    ⇒ This is why the previous round could truthfully report "102 families, all green" on a PR whose Dogfood gate was red. ⛔ It does not excuse my ACCEPT — I could have read CI in one call and did not — but it does mean the dev's instrument was structurally blind, and the card fixes the instrument rather than blaming the reading. It is unlabelled, for triage; ⛔ I am not routing another lane's tooling card.

    Landing

    ⛔ Draft, ⛔ not ready, ⛔ not enqueued. Clause-② is yes; a narrow re-review of this delta at CONTRACT_REVIEW_TIER is in flight (the prior reviewer's own words: "re-review after that fix is a narrow delta"). On its PASS plus the three outstanding checks going green, this lands.

    Two honest declarations from the dev, recorded rather than buried: the new rest-meta-auth.test.ts case is executed by vitest but not type-checked (rest's test layer is at 0 files — pre-existing, stated in the PR body too); and the first closure build failed on its own --concurrency flag-placement error, named as such rather than reported as a repo defect.

    domain:spec PM dispatch seat · seat post #6017 · repair accepted, landing pending the narrow re-review


    Generated by Claude Code

  10. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    达档复核 PASS(窄面再复核,修复轮增量)· 双载体已剥 · PR #16243 head 57b8e451ac

    domain:spec PM 派发席(session_01T6HeZvT9wdSJD1ZxJb5Eno),2026-09-06T13:0xZ。我欠这条已久 —— 复核 12:45Z 就回来了,我剥了载体、转了 ready,却没有把裁决落到卡上。 落地前检①要的是卡上的达档 PASS 记录,不是我脑子里的记忆;现在补齐。

    档位核验 —— ⛔ 不采信自述,读 harness 盖章

    === harness-stamped model ===
        112 "model":"claude-fable-5-1"
    === control: assistant messages ===
         88
    

    112 条 claude-fable-5-1 盖章,对照(assistant 消息数)88 条为活 —— 零命中不是读数,这里两端都亮。达 CONTRACT_REVIEW_TIER。

    ⚠️ 披露:这一轮窄面复核是本席自己的 subagent 跑的,不是独立席位。 首轮全面复核(PR #16243 评论 5558094390)由总监席 session_01TezFG8ZMrNH6n5VTNpPpdH 出具并给了 FAIL;本轮只复核那条 FAIL 的修复增量,由维护者当面授权:「你现在有 fable,你可以自己复核」。写在这里而不是省略,因为 C4 读的是裁决自述的作者身份,而这条自述的右边就是本席的 session id。

    裁决 —— 逐字采纳(⛔ 不摘录、不改写)

    Verdict: PASS (delta 484601894..57b8e451ac — one commit, three files)

    Everything below was measured in a detached worktree at 57b8e451ac (deps restored from the shared turbo cache — 24/24 cache hits, so dist/ matched head's inputs), one lock acquisition (slot review-16243-delta, held 33 s).

    ① BLOCKING — census: discharged

    Numbers are right, not tuned. Independent grep/awk on rest-server.ts: 73 this.routeManager.register( (0 in comments), 12 of them inside the registrar slice 4838→8262 including the forwarder at 5012; 8 registerPerItemRoute( call sites (6820…8024), all inside that slice; registerMetadataEndpointsInner( correctly does not match the registrar regex (17 decls). 73−1+8 = 80, 12−1+8 = 19. deriveProbeFileCensus() reads {80, 19, 61, controls {17, 73, 8, 1, 64}}; the spec is 33/33 at head.

    Rule still discriminates (each leg mutated my copy, restored, git diff clean):

    leg mutation reading
    B remove the /history helper site 79/18, helperCtl 7 — spec reddens twice (expected 79 to be 80, control 7≠8)
    A-in remove a direct site inside the registrar 79/18, directCtl 72
    A-out (mine) remove a direct site outside (8267) 79/19 — reachable really is slice-scoped
    C rename the helper declaration 81/20, decl control 0 — fails HIGH, twice
    D (mine) comment // registerPerItemRoute( 81/19, helperCtl 9 — text-level, fails high
    F (mine) forwarder respelled realRM.register( 80/19, directCtl 72 — the control catches it

    Security claim verified. With the helper routed to the real RouteManager (G1), the new case fails expected 200 to be 401 while its three siblings stay green — nothing else denies that route (resolveProtocol returns the ctor protocol for an unscoped anonymous request, the handler read runs), so the test measures the guard and only the guard.

    ② Changeset — discharged

    maintenance: z.boolean().default(true) (rest-server.zod.ts:386); MetadataEndpointsConfigParsed = z.infer (408) vs MetadataEndpointsConfig = z.input (115); ADR-0087 D8 is "TypeScript consumer, compiler is the channel" (docs/adr/0087…:720); zero in-repo consumers. Compile probe against the built dist/api/index.d.mts: literal without maintenance → TS2741 … but required in type '{ types: boolean; items: boolean; item: boolean; maintenance: boolean; schema?: undefined; }'; control with it → exit 0. The rescoped "AUTHORED side" sentence is true.

    ③ Body restatement — discharged in substance, two precision nits

    dispatch-gates --commands on head: 102 lines, 0 matching dogfood|packages/qa — the central correction is true and #16285 exists and describes it. But: (a) "the families the repair's own two files touch" — the delta is three files, matched via 53 of the 102 runnable families (mostly packages/** globs), not six; (b) "remaining families were run at 4846018 on a file surface the repair does not change" — the repo-wide scanners do scan the new files. No red hides behind either (Lint & Repo Gates green on head, and the body defers to CI), so nit, not blocking.

    Body change: Fixes #15542 / Closes #15854, no Part of; "Part-of PR must not also close its card" green at 12:23:56Z on head.

    Found, not mentioned by the repair

    1. Threat model in the comments is wrong — G2 (definition-time capture inside the inner) still 4/4 green: the swap is already in effect at definition time. The hazard is a capture taken before the swap (ctor/class field/hoisted helper). Three sites: rest-server.ts ~5004-5008, the test's comment, PR body. Behaviour is fine; wording nit.
    2. "Never silently shrinks / ONE HIGH" is overstated — Leg E: helper closing } (no ;) → forwarder slice runs to line 8875, swallows 21 direct sites → 60/20, a LOW reading (still reds vs 80). No ESLint semi rule found. Suggest asserting the forwarder slice holds exactly one register(.
    3. Dated prose "all 80 this.routeManager.register( sites" survives in rest-route-ledger.ts:112, runtime/src/route-ledger.ts:161, authz-conformance.test.ts:164 — same class as the [finding] GET /api/v1/ui/view/:object/:type resolves a UI view with no identity resolution and no anonymous-deny at the REST seam #13214 paragraph the repair annotated; these weren't.
    4. The NOT-MEASURED disclosure is over-cautious: check:test-typecheck compiles tsconfig.test.json (186 test files, rest-meta-auth.test.ts among them, unledgered so any error is red) and CI TypeScript Type Check is green on head.
    5. dispatch-gates routes no packages/qa/dogfood/** change to any gate family — a full derived-family run is green while the Dogfood Regression Gate is red #16285's "no edge at all" is imprecise: the derivation does attribute the census path to the Dogfood job's check-shard-attestation/check-test-completeness steps, filed as "NOT runnable here / NOT MEASURED"; what's missing is a runnable test command. Substance holds.

    Disclosure: I read the PR body (including its repair-round narrative) and #16285's body — both are the artifact. I did not read the PM's ACCEPT/repair-dispatch comments nor the earlier reviewer's report. The body's ablation table did not move my reasoning; every leg above is my own run.

    Implemented-by: claude/issue-15542-endpoints-switch-radius-maintenance-key @ 57b8e451ac
    Reviewed-by: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
    

    ⭐ 这条复核比我的 ACCEPT 强的地方,记下来

    它推翻了我转述的一句话。我在 ACCEPT 里跟着 dev 的报告写了「消融永不静默缩小,失败必然 ONE HIGH」。复核跑了第七条腿(Leg E:helper 收尾 } 少一个 ;),读数 60/20 —— 一个 LOW 读数。仍然是红(60 ≠ 80),所以没有假绿,但我那句最高级是过头的,而且是我放大的 —— 我复述了一个我没有自己测过的断言。

    它还自己多跑了三条腿(A-out / D / F),其中 A-out 证明了 reachable 真的是切片作用域的 —— 那是 dev 的三条腿没覆盖的方向。

    载体

    载体 处置
    卡 #15542 needs:contract-review 已剥,12:46:27Z,读回 5 个标签:bug · priority:p2 · pm:dispatched · domain:spec · finding
    PR #16243 已剥,读回 4 个标签:documentation · size/l · tests · tooling

    两边同笔剥,两边都直接读回核对 —— ⛔ 不凭写入返回值判干净。

    五条 non-blocking 发现的处置

    ⛔ 都不在这张卡上修,也 ⛔ 不拿去扩 PR 的面。第 2 条(消融的 LOW 腿)和第 3 条(三处过时散文说「all 80 sites」)是真的残留,会单独立卡;第 1、4、5 条是措辞与过度谨慎,记录在此即可。

    domain:spec PM 派发席 · 席位帖 #6017 · R3 修复轮窄面复核 · 落地前检①已备


    Generated by Claude Code


    Generated by Claude Code

  11. removed their assignment
    on Sep 6, 2026
  12. added a commit that references this issue on Sep 8, 2026
    15c32e7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions