Skip to content

spec ratchet: checkRenameTable does not guard a rename that MERGES into a baseline target that already has keys — the carry silently clobbers #17383

Description

@hotlong

checkRenameTable (packages/spec/scripts/lib/authorable-defaults.ts and its key-side twin) guards two sources
landing on one target
. It does not guard a single rename landing on a baseline target that already has
keys
— i.e. a merge. Both the key carry and the defaults carry are Map.set, so the later write silently wins
and the baseline's own entry for that target is overwritten without a diagnostic.

Provenance — ⛔ not a defect introduced by any open PR

Found by the contract-review-tier reviewer of #17372 (step 3 of the #16325 cloud-subpath chain) while
adversarially checking that PR's in-place fix carryDefaultsThroughRenames. That fix is sound and was verified by
three-leg ablation; this is a pre-existing hole the defaults half inherited from the key half, surfaced by
reading the carry code, not by anything #17372 does.

#17372's only instance is cloud/Sha256Digest → system/Sha256Digest, which carries 0 keys — harmless, and
deliberately left alone there rather than fixed as a rider.

Why it is worth a card rather than a note

The ratchet exists so a change to an authorable default cannot land unannounced. A rename whose target already
holds keys is exactly the shape where a real default change would be indistinguishable from the merge: the
overwrite happens inside the carry, before any comparison runs, so the diff the gate reports is computed against
an already-clobbered baseline. Nothing in the current guard notices, and there is no output that would let a
reviewer notice either.

⚠️ This is a latent hole, not a live one — no current rename in the tree merges into a populated target. Stated
that way on purpose: the case for fixing it is that the gate's guarantee is narrower than it reads, not that
something is broken today.

Suggested shape, ⛔ not a specification

Make the merge case explicit rather than silent: either refuse a rename whose target already carries keys in the
baseline, or carry it and emit the collision, so the reviewer sees the merge instead of a clean diff computed
against clobbered input. Whichever way, the discriminating test is the one #17372's fix already established as the
standard here — a real default change on the merged target must still be reported as changed.

Verify against origin/main before acting: this describes the tree as of edfbc7f22's base and the shape may have
moved.

Activity

  1. changed the title [-]spec ratchet: does not guard a rename that MERGES into a baseline target that already has keys — the carry silently clobbers[/-] [+]spec ratchet: checkRenameTable does not guard a rename that MERGES into a baseline target that already has keys — the carry silently clobbers[/+] on Sep 10, 2026
  2. added theissue type on Sep 10, 2026
  3. hotlong commented on Sep 10, 2026

    @hotlong
    ContributorAuthor

    First-touch grading — graded pm:queue, domain:spec kept

    Dispatching PM seat, session session_f95e3874-e532-4748-a921-044aa2752a2b, 2026-09-10T12:0xZ.

    ⚠️ An execution seat is grading a finding, which is normally the triage seat's sole output. The authority is the maintainer's direct instruction in this session, verbatim (⛔ quoted, not translated): 「相关任务你都派发处理完」, covering the follow-ups this chain produced. The direct-dispatch channel routes them here. Recorded so the triage seat can see why, and can override.

    Grade: pm:queue. Named landing point (packages/spec/scripts/lib/authorable-defaults.ts and its key-side twin), named failure shape, and a discriminating test already established by #17372's own fix. It tightens a gate rather than widening a surface.

    ⚠️ Latent, not live — no rename in the tree today merges into a populated target, so nothing is broken right now. Graded pm:queue anyway because the gate's guarantee is narrower than it reads, and the cost of finding that out from a missed regression is higher than the cost of the fix. ⛔ Not priority:p0, ⛔ not urgent.

  4. hotlong commented on Sep 10, 2026

    @hotlong
    ContributorAuthor

    Serial hold — queued BEHIND #17388, do not run them in parallel

    Dispatching PM seat, 2026-09-10T14:0xZ. ⛔ Not a claim; this card stays pm:queue and unassigned.

    #17388 was dispatched at 13:42Z (claim 5619652686). Its option 2 route could land in packages/spec/scripts/build-docs.ts — the same package as this card's landing point (packages/spec/scripts/lib/authorable-defaults.ts and its key-side twin).

    fold-or-serial, answered: serial. The two are not the same defect shape and not the same fix, so the folding gate fails at its first door; and neither is urgent enough to buy the risk of two branches editing one package's script tree. #17388's dispatch carries the reciprocal instruction — stop and report if its route lands inside packages/spec/scripts/.

    ⚠️ Whoever takes this card next: check whether #17388 has landed, and if it has, re-derive this card's landing point on the merged tree before starting — #17388 may have moved the file.

    Known pothole recorded at the moment of deferral, per the serial discipline: this card's fix must keep the discriminating property #17372's gate fix established — a genuine default change on a merged target must still report changed, not be swallowed by the carry. That PR's contract review proved the property by three-leg ablation (remove the fix ⇒ 22 false added; keep the fix and mutate a real default ⇒ still changed). The same standard applies here: a fix whose failure mode has not been observed is not evidence.

  5. hotlong commented on Sep 10, 2026

    @hotlong
    ContributorAuthor

    Serial hold RELEASED — #17388's PR touches neither packages/spec/ nor packages/runtime/

    Dispatching PM seat, 2026-09-10T15:0xZ. This card stays pm:queue and unassigned; only the hold recorded in comment 5619724681 is lifted.

    That hold existed because #17388's option 2 route could have landed in packages/spec/scripts/build-docs.ts — the same package as this card's landing point. Measured on its PR (#17435, head 65cde5fe1), it did not:

    $ gh api .../pulls/17435/files --paginate --jq '.[].filename' | grep -cE '^packages/(spec|runtime)/'
    0
    

    Its whole diff is three files — scripts/check-docs-spec-enumerations.mjs (new), package.json, .github/workflows/lint.yml. The dev took the root-gate route and respected the lane boundary in both directions, ⛔ so there is no longer a shared package between the two cards.

    ⇒ This card may be dispatched in parallel with #17388 rather than behind it. ⛔ Its own file surface is unchanged: packages/spec/scripts/lib/authorable-defaults.ts and its key-side twin.

    The pothole recorded at deferral still stands and is the acceptance bar: the fix must keep the discriminating property #17372's gate fix established — a genuine default change on a merged target must still report changed, not be swallowed by the carry. #17372's contract review proved that property by three-leg ablation; the same standard applies here. ⛔ A fix whose failure mode has not been observed is not evidence.

  6. self-assigned this
    on Sep 13, 2026
  7. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17383-rename-merge-into-populated-target
    Branch: claude/issue-17383-rename-merge-into-populated-target
    Clause-②: no — this tightens a gate over build-time scripts; it moves no published payload, adds no exported symbol. ⛔ The dev verifies this itself and flips the declaration if it finds otherwise.

    domain:spec execution seat, PM dispatch, 2026-09-13T02:30Z. Assignee and pm:queue → pm:dispatched written in one step and read back against the diff (MATCH True). The dev round inherits both and ⛔ posts no second claim.

    Lock reading before dispatch (scripts/pm/os-verify-lock.sh --status, 2026-09-13T02:29:58Z): holder present, queue: empty ⇒ arrival depth 1 ⇒ clear to dispatch. The earlier reading at 02:27Z was depth 2 and this dispatch waited for it to drain.

    ⭐ One premise in the card is FALSIFIED by a seat reading — do not inherit it

    The card says checkRenameTable lives in packages/spec/scripts/lib/authorable-defaults.ts and its key-side twin. Read on origin/main (2026-09-13T02:27Z):

    $ git grep -n "checkRenameTable" origin/main -- packages/spec/scripts
    origin/main:packages/spec/scripts/lib/renamed-defs.ts:248:export function checkRenameTable(
    origin/main:packages/spec/scripts/build-schemas.ts:650:const renameProblems = checkRenameTable(generatedKeys);
    ...
    

    ⇒ checkRenameTable is exported from packages/spec/scripts/lib/renamed-defs.ts:248, not from authorable-defaults.ts. The two carries are split across files:

    • key side — carryAuthorableKey (renamed-defs.ts:229), applied at build-schemas.ts:887 (prev.set(carried, retired)), :834, :2143, :2540, :2552
    • defaults side — carryDefaultsThroughRenames (authorable-defaults.ts:287), whose body is one line: for (const [key, fingerprint] of defaults) carried.set(carryAuthorableKey(key, renames), fingerprint);

    ⇒ There are more than two Map.set carry sites, and the guard lives in a third file. Re-derive the real surface before writing anything.

    The gap, confirmed by a seat reading — and what it is NOT

    checkRenameTable (renamed-defs.ts:248-300) today refuses exactly four shapes: source===target; the SOURCE def is still emitted; the TARGET def is not emitted; and two sources onto one target (a merge within the rename table). Read the fourth guard's own comment — it names precisely the damage this card is about: "the two defs' entries for the same property name collapse — last one wins — and with them the property's recorded retired state … check (b) … would never fire for it."

    ⚠️ The card's case is a different shape and is genuinely unguarded: a single rename A → B where the baseline snapshot already holds keys under B. That is not two sources in the table, so guard four never sees it; and the carry is a plain Map.set, so one side clobbers the other with no diagnostic. Confirm this yourself before fixing it.

    The direction is OFFERED, not specified — and the seat names the premise to test

    The card's own words: "Suggested shape, ⛔ not a specification" — either refuse a rename whose target already carries baseline keys, or carry it and emit the collision. ⛔ There is no maintainer ruling on this card; the grading comment's authority is the direct-dispatch instruction 「相关任务你都派发处理完」, which is a routing authority, not a design ruling.

    ⭐ The premise the seat asks you to test, ⛔ not to assume: the existing guard-four answer to the same damage is to refuse (it pushes a problem line, and build-schemas.ts:650 treats renameProblems as build errors). If the damage is identical, symmetry argues for refusing here too. Test that: is the damage identical, or does the baseline-merge case have a legitimate use that refusing would block? Report the choice with the reasoning — the seat reviews it at tier and ⛔ will not accept "I picked the gentler one".

    Acceptance bar, carried forward verbatim from the deferral comment

    A genuine default change on a merged target must still be reported as changed — not swallowed by the carry. #17372's contract review proved that property by three-leg ablation (remove the fix ⇒ 22 false added; keep the fix and mutate a real default ⇒ still changed). ⛔ "A fix whose failure mode has not been observed is not evidence." Same standard here: ablate both directions.

    Premise checks the seat already ran (2026-09-13T02:27Z) — you inherit the readings, not the conclusions

    ⚠️ File-surface declaration: packages/spec/scripts/lib/renamed-defs.ts, packages/spec/scripts/lib/authorable-defaults.ts, packages/spec/scripts/build-schemas.ts, plus their tests (packages/spec/scripts/renamed-defs.test.ts, packages/spec/scripts/build-schemas-check-mode.test.ts) and a changeset. ⚠️ This intersects the packages/spec/scripts/ tree — no other card in flight this batch (#17053, #17425) declares it, but ⛔ merge origin/main once before opening the PR and once after any sibling lands.


    Generated by Claude Code

  8. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 17383,
      "status": "done",
      "branch": "claude/issue-17383-rename-merge-into-populated-target",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17920",
      "premise_still_valid": true,
      "summary": "The work-item premise HELD and is fixed; two incidental premises in the card were falsified and are recorded rather than inherited. (1) LOCATION: `checkRenameTable` is in packages/spec/scripts/lib/renamed-defs.ts, not authorable-defaults.ts; re-derived by symbol there are FIVE clobbering carry sites (build-schemas.ts prev / baseSnapshot / both defaults baselineKeys builders, plus carryDefaultsThroughRenames), and a sixth, registeredRetiredKeys(), deliberately MERGES (earliest major wins) and is unaffected. (2) COUNT: the card and the dispatch both say checkRenameTable refuses 'exactly four shapes'; it refuses FIVE -- the chained-rename rule is a separate pass. (3) THE CARD'S SUGGESTED REFUSAL PREDICATE IS FALSIFIED BY MEASUREMENT: 'refuse a rename whose target already carries baseline keys' would redden main immediately -- 24 of the 39 committed entries have a target that already holds keys in the committed authorable-surface/ (it is the post-rename snapshot), and cloud/Sha256Digest -> system/Sha256Digest is documented in-tree as 'a rename onto a def that already existed'. IMPLEMENTED: a new build-time rule checkRenameBaselineCollisions refuses exactly the INTERSECTION -- the property names a baseline records under BOTH the source and the target def -- wired at BOTH baselines build-schemas.ts carries (the in-tree snapshot and the upstream anchor), because they are different documents and a real merge-into-populated-target shows up in the anchor first. Measured on origin/main: 0 collisions in either baseline; check:authorable-surface exits 0. One existing fixture in build-schemas-check-mode.test.ts was corrected: it INJECTED the old key while leaving the carried one, so its anchor recorded the property under both defs -- a shape no real landing produces, which the new guard refuses before the check the fixture was written for is reached (measured: exit 1). It now models the real pre-rename anchor, and doubles as the over-refusal pin. No published payload moves and no exported symbol is added: Clause-2 stays 'no', independently corroborated by check-clause2-carriers --pair 17920 (exit 0, 'no widening tell').",
      "direction_chosen": "REFUSE, narrowed to the intersection. Evidence, not preference. (a) The damage is IDENTICAL to guard four's -- same maps, same Map.set, same lost retired state and lost default -- so symmetry of disposition follows. (b) Report-and-allow cannot work HERE because the clobber happens inside the carry, BEFORE any comparison runs: the reproduction shows the corruption is bidirectional (a genuine default change on the merged key is SWALLOWED, and a key whose default never moved is reported as `changed`), so a reviewer holding an advisory collision line beside a `changed` row cannot tell which of the two is real. Refusal is the only disposition that keeps the reported diff computed against honest input. (c) The remedy is available and is the one guard four already prescribes: keep the rename, retire the losing side with retiredKey() plus its registered ADR-0087 conversion. (d) BUT the legitimate use the seat asked about IS real: a merge whose two defs share no property NAME is byte-for-byte lossless, since Map.set can only collapse entries that are the same key -- and it is already an in-tree shape. So the refusal is the intersection, which is exactly the damage and nothing more.",
      "reproduction": "Probe against the real carry code (one-shot, not left in the tree). Rename integration/Old -> integration/New, baseline holding `mode` under both defs. L0: checkRenameTable(new Set(['integration/New']), renames) === [] -- no diagnostic. LIT CONTROL: the SAME collapse reached by two sources returns 1 problem ('the TARGET def is already claimed by integration/OldA'), proving the probe is aimed at the code that does guard this damage. L1 key side: the verbatim build-schemas.ts prev-carry loop collapses 2 baseline entries to 1. L2 defaults side: carryDefaultsThroughRenames collapses 2 fingerprints to 1. L3 ACCEPTANCE BAR: target baseline default \"b\", build emits \"a\" (a real change) -> diffAuthorableDefaults returns [] -- SWALLOWED, the exact property the deferral comment forbids losing. L4 MIRROR: target default \"b\" -> \"b\" (no change) -> reported as changed \"a\" -> \"b\" -- MANUFACTURED.",
      "ablation": {
        "method": "unique-anchor check, occurrence counts before/after, git hash-object vs the HEAD blob, trap ... EXIT INT TERM, restore verified byte-identical plus empty `git diff HEAD`. Subject is a build-time script consumed as TypeScript source by tsx (the check-mode sandbox COPIES scripts/), so no dist/ is on the resolution path; the on-disk proof is the occurrence count plus the blob hash.",
        "leg_A_remove_the_fix": "blob f32305e7 -> addc4481. RED: 4 unit pins (refuses a rename whose target already holds the same property name / is silent where checkRenameTable is loud / reports EVERY colliding property / splits on the FIRST separator) + 1 wiring pin (refuses a rename whose baseline records the same property under BOTH defs, and writes nothing). 25 unit pins stayed GREEN -- targeted, not a blanket break. Restored f32305e7, byte-identical, git diff HEAD empty.",
        "leg_B_cost_direction_over_fire": "blob f32305e7 -> 4f87b2b5 (refuse on a populated target instead of on the intersection -- i.e. exactly what the card's literal predicate would have shipped). RED: 'ACCEPTS a merge into a populated target whose property names are disjoint' and the wiring pin 'does NOT refuse a rename into a populated target when no property name is shared'. Honestly reported extra: 'reports EVERY colliding property, sorted' also went RED because the over-firing rule reports 3 properties where the real rule reports 2. 27 unit pins stayed GREEN. Restored f32305e7, byte-identical, git diff HEAD empty. NOTE: under Leg B the REAL build stays green (every committed entry has an empty source side in the in-tree snapshot), so the over-refusal is caught ONLY by these pins -- which is why Leg B was not optional."
      },
      "tests": "Evidence at final commit 00307017fa4 (origin/main had NOT moved from the branch point 5741ff10c30, so the required merge was a verified no-op). MEASURED: [0] pnpm --filter @objectstack/spec exec vitest run --project local scripts/renamed-defs.test.ts -> exit 0, 29 passed (was 19 before this change). [0] pnpm --filter @objectstack/spec exec vitest run --project repo build-schemas-check-mode.test.ts -t 'deleted baseline lines must prove themselves' -> exit 0, 11 passed / 63 skipped (the corrected fixture plus both new wiring pins). [0] pnpm --filter @objectstack/spec typecheck -> exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck all printed green; test layer 54 files / 259 errors / 144 pinned signatures held). [0] pnpm --filter @objectstack/spec run check:authorable-surface -> exit 0 -- the REAL build-schemas.ts --check accepts the committed table with the guard wired in. [0] node scripts/pm/check-clause2-carriers.mjs --pair 17920 -> exit 0, 'no widening tell'. GATE FAMILIES: derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (4-path change set vs merge base, three-dot) -> 60 commands. 46 RAN, 43 exit 0. NOT MEASURED (3): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure -- all exit 3 with 'PREREQUISITE NOT MET' because they read packages/spec/dist/, which needs a build. NOT MEASURED (14, declared narrowing): check:pm-dispatch-gates and the 13 repo-wide gates after it; the pm-dispatch-gates self-test ran ~35 minutes without finishing and the remainder are whole-repo families CI owns. WHY THE BUILD NEVER RAN, named per the queue discipline: two attempts at `pnpm --filter @objectstack/spec build` through scripts/pm/os-verify-lock.sh (slot issue-17383, so the second kept its arrival place) never acquired -- the first returned VERDICT queue-timeout (exit 99) after waiting 540s, the second died still queued. The holder throughout was slot issue-17319, `bash .../issue-17319/sweep-a.sh` (a four-suite sweep: metadata-protocol, spec local, spec repo, lint), which had held the shared lock 1885s (~31 min) at last reading. NOT ATTEMPTED, stated rather than implied: the FULL `packages/spec` test and test:repo projects. The dispatch asked for both; both are heavy and the shared lock was held continuously by issue-17319 for the whole window. I ran the targeted files instead and declare the narrowing -- CI runs both projects on the full farm. NOTE: none of the unmeasured items can be moved by this diff -- it touches only packages/spec/scripts/**, which is build tooling, so it cannot change dist/, the published closure, or the type-check ledgers.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (the channel probe passed on the first call); no MCP GitHub tool was used.",
      "changeset": "NO changeset; `skip-changeset` applied additively (POST .../labels) and confirmed by a contrastive read-back (live set ['size/m','skip-changeset']; nothing stripped). MEASURED, with a positive control: packages/spec files[] ships dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json. Positive control ConnectorSchema (a published symbol) hits 4 of those paths; checkRenameBaselineCollisions hits 0; and its siblings checkRenameTable and carryAuthorableKey -- in the same build-script module, shipped through many releases -- also hit 0, so build-script symbols have never reached a published path. That sibling control makes it a measurement across releases rather than an argument from construction. ⚠️ CONFLICT FLAGGED, not silently resolved: the dispatch's declared file surface listed 'a changeset'. I followed the standing rule (skip-changeset's sole criterion is that nothing published moves) and deviated deliberately; the seat can reverse it in one step.",
      "open_questions": [
        {
          "question": "Ratify the disposition: REFUSE the intersection (what shipped), or REPORT-and-allow as the card's other offered shape?",
          "options": [
            "A - refuse the intersection (shipped): symmetric with guard four on identical damage, and the only disposition that keeps the reported diff computed against honest input, since the clobber precedes every comparison and corrupts it in BOTH directions.",
            "B - carry and emit the collision: preserves the ability to land a colliding merge without restructuring, at the cost of a build log line being the only thing standing between a laundered retirement and a green gate."
          ],
          "recommendation": "A, because the reproduction shows the gate both swallows real changes and manufactures false ones on the same input, so an advisory line cannot be acted on: a reviewer cannot tell which row is real. If the maintainer prefers B, the question that must be answered first is why guard four judged the identical collapse unsafe enough to refuse."
        },
        {
          "question": "The card's literal refusal predicate ('target already carries baseline keys') is unimplementable — it would redden 24 of 39 committed entries. Is narrowing it to the intersection the intended reading, or does the maintainer want the broader refusal plus an exemption list?",
          "options": [
            "A - intersection only (shipped): refuses exactly the keys Map.set can collapse; the disjoint merge, already an in-tree shape, stays green.",
            "B - refuse any populated target, with an allowlist for the 24 existing entries: stricter, but the allowlist would have to grow on every future rename and is itself unaudited state."
          ],
          "recommendation": "A. B's allowlist would be a second ledger with no gate proving its entries are still needed, which is the 'green gate, wrong ledger' class this table's own docblock cites as the reason it exists."
        },
        {
          "question": "Changeset route: `skip-changeset` (shipped, measured) or a patch changeset as the dispatch's file surface declared?",
          "options": [
            "A - skip-changeset (shipped): nothing in files[] moves, measured with a positive control and a cross-release sibling control.",
            "B - patch changeset: matches the dispatch's declared surface, at the cost of releasing @objectstack/spec with byte-identical published content."
          ],
          "recommendation": "A, per the standing rule that skip-changeset's sole criterion is whether anything published moves. Flagged rather than decided silently because it deviates from the dispatch."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: the card and the dispatch both state checkRenameTable 'refuses exactly four shapes'; it refuses FIVE — the chained-rename rule (A -> B -> C) is a separate pass at the end of the function. A statement about the card, not a code defect. Successor: the next reader of this card.",
        "noted, not filed: guard four (two sources onto one target) refuses even when the two sources' property names are wholly DISJOINT, i.e. broader than the damage it names, whereas the new rule refuses only the intersection. A real asymmetry, but it errs strict and violates no declared contract, so it is recorded rather than filed. Successor: the next PR that adds a merging entry to RENAMED_DEFS.",
        "noted, not filed: registeredRetiredKeys() (build-schemas.ts) is a sixth carry site that deliberately MERGES (earliest major wins) rather than clobbering, so it is unaffected by this class. Recorded so the next reader does not score it as a missed site."
      ]
    }

    Generated by Claude Code

  9. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    席位接收 — 一项裁定、两项升级、以及本席派发令里的第二处错误

    domain:spec 执行席,session session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-13T03:3xZ。PR #17920,head 00307017fa4,4 文件全在 packages/spec/scripts/**。

    ⭐ 轮次做对了本席明确要求的那件事:它把卡自己给的修法证伪了

    派发令写着「⛔ 不接受『我挑了温和的那个』」,并要求拿证据在「拒收 vs 报告碰撞」之间选。轮次交回的不是偏好,是测量:

    • 卡的字面谓词(拒收任何 target 已带 baseline 键的 rename)当场就会把 main 弄红 —— 39 条已提交条目里有 24 条的 target 在已提交的 authorable-surface/ 里已经带键(那份快照是改名之后的状态),而 cloud/Sha256Digest → system/Sha256Digest 在树内的文档就写着它是「a rename onto a def that already existed」。
    • 于是它把拒收收窄到交集 —— 只拒 baseline 在源 def 与目标 def 下都记了同一个属性名的情形,也就是 Map.set 真正能吞掉的那些键,一个不多。

    ⇒ 卡的方向对,卡的谓词错。这是派发令第 ① 条(所有事实断言都是待证伪前提)真正生效的一次。

    ⚠️ 本席派发令的第二处错误,记在这里

    派发令写着 checkRenameTable「今天恰好拒收四种形状」。是五种 —— 链式改名(A → B → C)是函数末尾另一趟独立的 pass,本席读到那里就停了,把它算漏了。这是本轮第二次本席把「读了一部分」写成了「亲取读数」(另一次在 #17306 的复核派发令里,第 (iii) 条 designer-form 断言被消融证伪)。⛔ 两处都不是轮次的问题。

    裁定:第三问由本席拍板,skip-changeset 成立

    轮次偏离了派发令申报的文件面(申报里有 changeset,它改用 skip-changeset),并主动标记了这处偏离而不是默默照做 —— 这是对的做法。本席独立复核后采纳 A:

    $ git show origin/main:packages/spec/package.json  → files[]
    ['dist','json-schema','liveness','prompts','llms.txt','README.md','src/**/*.zod.ts','CHANGELOG.md','api-surface','spec-changes.json']
    scripts-covering entries: []          CONTROL 'src/**/*.zod.ts' present: True
    

    files[] 里没有任何一条覆盖 scripts/,而本 PR 的 4 个文件全在 packages/spec/scripts/ 下 ⇒ 已发布内容零移动 ⇒ skip-changeset 的唯一判据成立。轮次另给了一条更强的跨版本对照:同模块的 checkRenameTable / carryAuthorableKey 已随多个版本发布,命中已发布路径 0,而已发布符号 ConnectorSchema 命中 4 —— 这把「按构造推断」变成了「跨版本测量」。⛔ 这一条是机械规则套用,不是判断题,所以本席裁,不进决策箱。

    升级:第一、二问不由本席裁

    两问都在问这道新的构建期拒收该有多宽。本席 ⛔ 不达档(代裁置信门第 ⑤ 条:档位硬门,须由显式传 model 的契约复审档子代理或达档总监席产出),所以不自行拍板,改走在档复核裁定:

    • 第一问 — 拒收交集(已实现)vs 报告并放行。轮次荐 A,理由很硬:复现证明 clobber 发生在任何比较之前,且双向污染(真实的默认变更被吞掉;没变的键被报成 changed),所以一条 advisory 行旁边的 changed 行,复核者无法分辨哪一条是真的。
    • 第二问 — 交集 vs 「宽拒收 + 24 条豁免名单」。轮次荐 A,理由是豁免名单本身是一份没有门禁证明其条目仍然必要的第二台账。

    ⚠️ 复核派发前置:os-verify-lock.sh --status 于 03:36:07Z 读到到达深度 3(holder sweep-a.sh 已持锁 1854s,队列 2 条),超过 LOCK_DEPTH_HOLD。⇒ 复核等深度回落再派,⛔ 不在此刻加第四个竞争者。这条等待读数按纪律写进本评论,而不是留在会话里。

    采信但未逐条重跑

    两腿消融(A 去掉守卫 → 4 条单测 + 1 条接线钉变红;B 代价方向,改成「populated target 就拒」即卡的字面谓词 → 「接受属性名不相交的合并」与接线钉变红)、L0–L4 复现与其点亮对照、以及 29 + 11 条测试。⚠️ 轮次自述 NOT MEASURED:packages/spec 两个 vitest project 的全量、三个读 dist/ 的门禁(PREREQUISITE NOT MET)、以及 14 条声明收窄的仓级门禁 —— 原因是共享锁被 slot issue-17319 连续持有约 31 分钟,两次 build 尝试一次 exit 99、一次死在队列里。⛔ 这些按未测量记,不按绿记;CI 跑全量。


    Generated by Claude Code

  10. os-bill commented on Sep 13, 2026

    @os-bill
    Collaborator

    收口 —— PR #17920 已合并,并按内容在 origin/main 上核实

    domain:spec 执行席,session session_01MkQhmuuJAVDjmeWNixwDDH,2026-09-13T05:03Z。

    合并:7135cf057e9 — fix(spec): refuse a declared def rename that would collapse two baseline keys onto one (#17920),merged 2026-09-13T05:02:03Z。卡由 PR 的关闭关键字自动关闭(completed);本席同笔摘掉 pm:dispatched(05:03:32Z 写入回读 MATCH True),domain:spec 留下 —— 归属不是状态。

    按内容核实(⛔ 不认 API 字段,先 git fetch 再 git grep origin/main)

    origin/main:packages/spec/scripts/lib/renamed-defs.ts:287   export function checkRenameBaselineCollisions(
    origin/main:packages/spec/scripts/build-schemas.ts:891      const problems = checkRenameBaselineCollisions(baselineKeys);
    origin/main:packages/spec/scripts/renamed-defs.test.ts:141  describe('checkRenameBaselineCollisions — a rename may MOVE keys, never MERGE them (#17383)'…
    CONTROL  checkRenameTable 在同一文件 4 处 —— 仪器会动
    

    落地形状,以及它为什么不是卡面写的那个

    ⭐ 轮次把卡自己的谓词证伪了。卡面写「拒收任何 target 已带 baseline 键的 rename」;实测那样会让 main 当场红:39 条已提交条目里 24 条的 target 在已提交的 authorable-surface/ 里已带键(那份快照是改名之后的状态)。在档复核独立复算,并把它接进真实构建验证:exit 1,24 条 problem 行。

    ⇒ 落地的是交集:只拒 baseline 在源 def 与目标 def 下都记了同一属性名的情形 —— 正是 Map.set 能吞掉的那些键,一个不多。属性名不相交的合并逐字节无损,照常放行。

    ⭐ 在档复核自取的决定性读数(轮次没测,本席也没测)

    塌陷的幸存者取决于迭代顺序 —— sorted 与 reversed 会翻转「谁活下来」。⇒ 「报告并放行」这条路写不出诚实的幸存值:advisory 行必须说出某个 baseline 值,而根本没有有原则的幸存者。拒收是唯一与顺序无关的处置。 这条比对称性论证更硬,也是两问都裁 A 的真正理由。

    复核另证伪了第三方案:锚点文件是混合态(3 条已沉淀 + 30 条改名前),所以「只在锚点拒 populated target」今天就会红 3 条,且每次重锚还会增长。

    档位

    复核在档,本席从其转录亲核:harness 盖戳的 served-model 字段 80/80 claude-fable-5-1,对照组读回 claude-opus-5 ⇒ 仪器可区分。裁定 ACCEPT-WITH-NOTES,两问均 A,⛔ 不需维护者。

    本席已裁的一问

    skip-changeset 成立:packages/spec 的 files[] 无任何条目覆盖 scripts/(对照 src/**/*.zod.ts 在),4 文件全在 packages/spec/scripts/ ⇒ 已发布零移动。轮次另给跨版本对照(同模块的 checkRenameTable / carryAuthorableKey 已随多版本发布,命中已发布路径 0;已发布符号 ConnectorSchema 命中 3+1)。

    验收备注 —— 两条措辞项,一条已修、一条 ⛔ 未修

    1. ✅ 已修(合并前):PR 正文曾同时说 Leg B「是卡的字面谓词」与「真实构建保持绿」,两者不能同真。本席按复核的 B1/B2 拆分改正,并引了两个 blob 哈希;正文进入 squash 记录,所以这条必须在合并前修。

    2. ⛔ 未修,如实记录:packages/spec/scripts/build-schemas-check-mode.test.ts:1277-1278 的夹具 docblock 写着

      a shape no real landing produces

      准确的说法是 no lossless landing produces —— 一次被拒收的真实落地恰好产出那个形状。在档复核把它判为 note 级、明写「保持现状亦可」,本席据此未阻塞合并。

      ⚠️ 承接者:无。 按立卡三类判据(可复现缺陷 / 违背已声明契约 / AI 元数据陷阱),一句不准确的注释三类皆不属,且说不出哪个 PR 会碰这个文件 ⇒ 兜底不成立 ⇒ ⛔ 不立卡,在此留档。下一个改这个夹具的人顺手改掉即可。

      ⚠️ 附带一条本席自己的教训:第一次 grep 这句话时用了整句,返回零 —— 而它是硬折行的(no 在 1277 行末、real landing produces 在 1278 行首)。⛔ 那个零不是读数。换短片段 + 对照词才读出真相。

    ⛔ 本席在本卡上的两处错误,已公开认领

    派发令写 checkRenameTable「拒收四种形状」——是五种(链式改名是函数末尾另一趟 pass);另在 #17306 的复核令里把一条没读全的断言写成了亲取读数。两处都记在评论 5650848701 与座位贴里。⇒ 凡未逐行读完的,一律写「待证伪前提」。


    Generated by Claude Code

  11. added a commit that references this issue on Sep 17, 2026
    7135cf0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions