fix(spec): the dropped-refinement ledger refusal names sites, the key it requires - #18816
Conversation
…ey it requires
`readDroppedRefinementsBaseline`'s shape diagnostic said the entries are
`key -> { count, reason }` while the very next check in the same function
requires `sites: string[]`, and the shipped `DroppedRefinementsEntry` carries
neither `count` nor `reason`. A ledger an author repaired by following that
sentence is refused again by the same function.
The module docblock carried the same vocabulary over from
`unemitted-schemas.ts`, the module this one was copied from: it promised a
per-entry `reason` the gate requires to be non-empty, which the
`DroppedRefinementsEntry` docblock seventy lines below explains at length is
deliberately absent, and it described the ratchet as re-checking a recorded
`count`. Both now describe the shipped shape: a non-empty `sites` list,
re-checked path by path.
The pin is a closed loop rather than a wording match — whatever the refusal
names has to be what the reader then accepts — with a lit control on the ledger
an author following the old sentence would have written.
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
…opped-refinements-error-shape
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
⭐ 更正:我上一条里纠正你的那句话,错的是我上一条评论(5722300053)我写了「那 243 条不可能是「committed entries」;它只能来自你本地跑生成器产出的那一份」,并说「数字也许没错,但「committed」这个词错了」。 这句话本身是错的。你的原话是对的, 重新测(2026-09-17T23:00Z, 243 条,一条不差,全部已提交。而且每一条的键集恰好是 对照组(证明读法有效,不是又一次假零):同目录的 这次假零的形状,值得单独记一笔我上一条犯的不是算错数,是把一次无效测量的沉默当成了证据,然后拿它去推翻一个人的原始读数。两处具体错误:
所以本条更正不改变对 #18816 的结论 —— PR 照旧接受,证据链只是比我上一条说的更干净:你引的 243 是一个真实的、已提交的计数。 顺带,两条我查过之后决定不立卡的既然重测了这棵树,把另外两条一并了结,免得留成悬案:
如果你不同意第二条的收口 —— 特别是你手上有我没测到的、真写过多余键的实例 —— 在这条下面说,我重开。 (本条已编辑一次:首发版本在「重新测(…)」那行里原样漏出了一个未被替换的 NOW 占位符(双花括号那种)。原因是我的发帖闸门调 另:修好后我重跑闸门,而闸门的最后一步是真的发帖而非只做 dry-run,于是多出了一条内容重复的 5722364389 —— 那条已被我改成一行指回本条的说明。本条是唯一的正文。) Generated by Claude Code |
|
(这条是我发帖时的重复件 —— 我修正上面那条时重跑了发帖闸门,而闸门的最后一步是真的发帖而非只做 dry-run。正文见 5722349441;本条内容已移除,只留这行指路。平台不提供删除接口,所以它只能以这个形状留在串里。) Generated by Claude Code |
Fixes #18747
Clause-②: no
Where the two defects actually are on
mainThe card's line numbers were taken on a branch head. Re-anchored by symbol against
origin/mainat the base of this branch (034f5a3afd), inpackages/spec/scripts/lib/dropped-refinements.ts:mainit is at:453:496throw new ErrorinreadDroppedRefinementsBaseline:456-458:499-501entry.sitescheck and the secondthrow new Error:68:66-70reason" sentence:134-150DroppedRefinementsEntrydocblock that contradicts itWhich side is true — measured from the consumers, not chosen
The card's open question was whether the refusal should name
sitesor the shapeshould really carry
count/reason, and which docblock governs. Every consumer thatreads this ledger answers
sites, and none of them readscountorreasonat all:DroppedRefinementsEntry(the shipped interface)readonly sites: readonly string[]packages/spec/dropped-refinements.baseline.jsonsites, 0 carrycount, 0 carryreasoncheckDroppedRefinementsentry.sitesonly — set difference plus a length compareunreasonedrefusalentry.sites.length === 0, and the gate prints "carry an emptysiteslist"build-schemas.tsremedy text (both the undeclared and the miscounted arms)"sites": [ ... ]descriptionfieldsitesand nothing elsedropped-refinements.test.ts"the committed ledger"entry.sites.lengthper entry and thatmeasured.droppedRefinementSitesequals their sumSo
sitesis the contract and theDroppedRefinementsEntrydocblock governs. The twowrong texts are both residue of the module this one was copied from: in
packages/spec/scripts/lib/unemitted-schemas.tsthe entries really are{ cause, reason }, its refusal really does say so, and its gate really does require anon-empty
reason(entry.reason.trim() === ''). Copying the file carried thevocabulary across without the shape.
LIT — the red leg, both messages verbatim
The trap is a sequence, so the probe walks it: take a structurally broken ledger, read
what the reader says the shape is, write that shape, and hand it back to the same
function.
BEFORE (the module restored to
origin/main, the rest of the tree unchanged):The repair written from the message is refused by the same function, four lines
below the message that prescribed it.
AFTER:
The mutation leg and the restore leg were each proved on disk (the deleted text present
and the injected text absent, then the reverse), and the restore was verified
byte-identical to
HEADbygit hash-object(9615f4e0c0…both sides) with an emptygit diff HEADand an emptygit status --porcelain. The probe itself lives outsidethe repository and nothing of it is committed.
DARK — a legitimate ledger passes on both legs, and nothing else moved
readDroppedRefinementsBaselinepnpm --filter @objectstack/spec testdropped-refinements.test.tsThe
+4is exactly the four tests this PR adds. The BEFORE row is a real run, notarithmetic: both files were checked out at the merge base, the suite was run, and both
were restored and re-verified byte-identical to
HEAD.Nothing else in the repository pins either message —
git grep "must be an object of key"returns exactly two hits, this one andunemitted-schemas.ts's own (which is correct for its own shape, and is the livecontrol on that grep).
The pin is a closed loop, not a wording match
Error prose is not pinned here on its spelling; what is pinned is the named subject
and the property that makes this class of defect a trap: whatever the refusal names has
to be what the reader then accepts. Four cases in
packages/spec/scripts/dropped-refinements.test.ts:sites;still says
sites(without this leg the first two pass on a reader that acceptsanything);
The docblock half has no pin, deliberately: no consumer parses a docblock, and a
source-text assertion over prose is a gate that fails on rewording rather than on
regression.
Also in this diff, declared
count" — the same contradiction as thereasonsentence the card names, in theparagraph above it, against the same
DroppedRefinementsEntrydocblock ("The unit isthe SITE and not a count, deliberately"). Repaired in place under the bounded
exemption: same defect class as this card, same file, mechanical, the corrected form
already fixed by the entry docblock, no new verification surface, and no other claim
holds any
dropped-refinements*path (measured across all 26 openclaude/issue-*PRs, with
proof-registry.mtsreading out for fix(spec): re-read four sharing proof-registry reasons now thatsharing_ruleis governed #18797 as the live control).packages/spec/scripts/dropped-refinements.test.tswas listed read-only on the claim.It is written here, and only to carry this card's own regression pin.
The card's second open question — is there a third site?
unemitted-schemas.ts, the sibling the docblock calls itself "Identical to", was read:it has the same defect nowhere. Its docblock claim, its refusal text, its interface
and its ledger all agree on
{ cause, reason }. It is the correct template, not asecond instance.
Changeset — measured, not inferred from the path
npm packonpackages/specafter a full build, then grep over the packed bytes(142,490,031 of them):
package/scripts/DROPPED_REFINEMENTS_BASELINE_FILEin the packed bytesreadDroppedRefinementsBaselinemust be an object of key -> { sites: string[] }package/src/package/json-schema/x-dropped-refinementsin the packed bytesObjectSchemain the packed bytespackages/spec/scripts/**is absent from the package'sfiles[], and the build afterthis change leaves
git statusclean, so nodist/byte moves either. Nothing thisdiff changes publishes from any released package, so it carries
skip-changesetratherthan a changeset.
Verification
pnpm --filter @objectstack/spec build— exit 0 (34/34 declaration files present).pnpm --filter @objectstack/spec test— exit 0, 487 files / 14055 tests.pnpm --filter @objectstack/spec typecheck— exit 0, includingtsconfig.scripts.json(which is what compiles the edited file) and the test layer.node scripts/pm/dispatch-gates.mjs --commandsderived 57 families from thechange set; all 57 were run and reconciled with
--ran: 54 exit 0, 3 exit 3 =NOT MEASURED (
check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt— each refuses its own prerequisite because onlypackages/specis built in this worktree; all three read built output, which thisdiff cannot move, and CI builds the full closure).
pnpm check:nul-bytesexit 0, plus a direct control-character scan of both editedfiles — no match, with a live non-zero control on a file that carries one.
origin/mainwas merged in before this PR was opened (clean, noos-regendeferral).Generated by Claude Code