Skip to content

fix(spec): the dropped-refinement ledger refusal names sites, the key it requires - #18816

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-18747-dropped-refinements-error-shape
Sep 17, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-18747-dropped-refinements-error-shape

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18747

Clause-②: no

Where the two defects actually are on main

The card's line numbers were taken on a branch head. Re-anchored by symbol against
origin/main at the base of this branch (034f5a3afd), in
packages/spec/scripts/lib/dropped-refinements.ts:

the card said on main it is at symbol
:453 :496 the first throw new Error in readDroppedRefinementsBaseline
:456-458 :499-501 the entry.sites check and the second throw new Error
module docblock :68 :66-70 the "Every entry carries a reason" sentence
(not named) :134-150 the DroppedRefinementsEntry docblock that contradicts it

Which side is true — measured from the consumers, not chosen

The card's open question was whether the refusal should name sites or the shape
should really carry count/reason, and which docblock governs. Every consumer that
reads this ledger answers sites, and none of them reads count or reason at all:

consumer reading
DroppedRefinementsEntry (the shipped interface) one member: readonly sites: readonly string[]
packages/spec/dropped-refinements.baseline.json 243 entries; 243 carry sites, 0 carry count, 0 carry reason
checkDroppedRefinements reads entry.sites only — set difference plus a length compare
the unreasoned refusal fires on entry.sites.length === 0, and the gate prints "carry an empty sites list"
build-schemas.ts remedy text (both the undeclared and the miscounted arms) prints the corrected entry as "sites": [ ... ]
the ledger's own description field documents sites and nothing else
dropped-refinements.test.ts "the committed ledger" asserts entry.sites.length per entry and that measured.droppedRefinementSites equals their sum

So sites is the contract and the DroppedRefinementsEntry docblock governs. The two
wrong texts are both residue of the module this one was copied from: in
packages/spec/scripts/lib/unemitted-schemas.ts the entries really are
{ cause, reason }, its refusal really does say so, and its gate really does require a
non-empty reason (entry.reason.trim() === ''). Copying the file carried the
vocabulary 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):

[step 1] input: {"entries": []}
  REFUSED   dropped-refinements.baseline.json: "entries" must be an object of key -> { count, reason }

[step 2] the ledger an author writes by FOLLOWING step 1
         input: {"entries":{"system/TraceSamplingConfig":{"count":1,"reason":"zod projects no custom check"}}}
  REFUSED   dropped-refinements.baseline.json: entry "system/TraceSamplingConfig" needs a `sites` array of path strings

The repair written from the message is refused by the same function, four lines
below the message that prescribed it.

AFTER:

[step 1] input: {"entries": []}
  REFUSED   dropped-refinements.baseline.json: "entries" must be an object of key -> { sites: string[] }

[step 3] the ledger an author writes by FOLLOWING step 1
         input: {"entries":{"system/TraceSamplingConfig":{"sites":["properties.rate"]}}}
  ACCEPTED  (1 entry/entries)

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 HEAD by git hash-object (9615f4e0c0… both sides) with an empty
git diff HEAD and an empty git status --porcelain. The probe itself lives outside
the repository and nothing of it is committed.

DARK — a legitimate ledger passes on both legs, and nothing else moved

reading BEFORE AFTER
the real committed ledger through readDroppedRefinementsBaseline ACCEPTED — 243 entries, 737 sites ACCEPTED — 243 entries, 737 sites
pnpm --filter @objectstack/spec test 487 files / 14051 passed, 0 failed 487 files / 14055 passed, 0 failed
dropped-refinements.test.ts 23 tests 27 tests

The +4 is exactly the four tests this PR adds. The BEFORE row is a real run, not
arithmetic: 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 and
unemitted-schemas.ts's own (which is correct for its own shape, and is the live
control 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:

  1. the shape diagnostic names sites;
  2. a ledger written to that shape is then ACCEPTED — the loop closes;
  3. LIT CONTROL — the shape the old diagnostic named is refused, and that refusal
    still says sites (without this leg the first two pass on a reader that accepts
    anything);
  4. the shape diagnostic names no key the entry shape does not have.

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

  • The module docblock's shrink-only bullet said the ratchet re-checks "a recorded
    count" — the same contradiction as the reason sentence the card names, in the
    paragraph above it, against the same DroppedRefinementsEntry docblock ("The unit is
    the 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 open claude/issue-*
    PRs, with proof-registry.mts reading out for fix(spec): re-read four sharing proof-registry reasons now that sharing_rule is governed #18797 as the live control).
  • packages/spec/scripts/dropped-refinements.test.ts was 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 a
second instance.

Changeset — measured, not inferred from the path

npm pack on packages/spec after a full build, then grep over the packed bytes
(142,490,031 of them):

reading hits
tarball entries under package/scripts/ 0
DROPPED_REFINEMENTS_BASELINE_FILE in the packed bytes 0
readDroppedRefinementsBaseline 0
must be an object of key -> { sites: string[] } 0
positive control — entries under package/src/ 203
positive control — entries under package/json-schema/ 1530
positive control — x-dropped-refinements in the packed bytes 486
positive control — ObjectSchema in the packed bytes 965

packages/spec/scripts/** is absent from the package's files[], and the build after
this change leaves git status clean, so no dist/ byte moves either. Nothing this
diff changes publishes from any released package, so it carries skip-changeset rather
than 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, including
    tsconfig.scripts.json (which is what compiles the edited file) and the test layer.
  • node scripts/pm/dispatch-gates.mjs --commands derived 57 families from the
    change 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 only
    packages/spec is built in this worktree; all three read built output, which this
    diff cannot move, and CI builds the full closure).
  • pnpm check:nul-bytes exit 0, plus a direct control-character scan of both edited
    files — no match, with a live non-zero control on a file that carries one.
  • origin/main was merged in before this PR was opened (clean, no os-regen deferral).

Generated by Claude Code

…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>
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

⭐ 更正:我上一条里纠正你的那句话,错的是我

上一条评论(5722300053)我写了「那 243 条不可能是「committed entries」;它只能来自你本地跑生成器产出的那一份」,并说「数字也许没错,但「committed」这个词错了」。

这句话本身是错的。你的原话是对的,committed 这个词用得准确。 我据以下结论的那次测量是一次假零 —— 我在一个并不指向 origin/main 的工作树上查文件,把「查不到」读成了「不存在」。

重新测(2026-09-17T23:00Z,origin/main = ad1f94e8ec,直接读 git 对象、不经工作树):

$ git cat-file -e origin/main:packages/spec/dropped-refinements.baseline.json && echo EXISTS
EXISTS                                    # 52339 bytes

$ node -e '<读 entries>'
entry count: 243
total sites: 737
per-entry key shapes: [ [ 'sites', 243 ] ]
entries with EMPTY sites: 0
top-level keys: [ 'description', 'measured', 'entries' ]

243 条,一条不差,全部已提交。而且每一条的键集恰好是 {sites} —— 没有任何一条带 countreason 或别的残留键。

对照组(证明读法有效,不是又一次假零):同目录的 unemitted-schemas.baseline.json 同样 EXISTS;两者都读到了,说明「读不到」不是这套读法的默认输出。

这次假零的形状,值得单独记一笔

我上一条犯的不是算错数,是把一次无效测量的沉默当成了证据,然后拿它去推翻一个人的原始读数。两处具体错误:

  1. /home/user/objectstack 的工作树并不在 origin/main 上。对它 grep -rn / 查文件存在性,得到的是另一棵树的答案,却被我当成了 main 的答案。同一次排查里我对 checkDroppedRefinementsgrep -rn . 也拿到了全仓零命中 —— 而 git grep origin/main 显示它明明在 build-schemas.ts:3429 被消费。同一个假零连中两次。
  2. 沉默(查不到 / 零命中)恒不是读数,除非同时有一个已知非零的对照组证明读法本身有效。第 2 点是我自己写进办法里的规矩,这次是我自己没执行。

所以本条更正不改变对 #18816 的结论 —— PR 照旧接受,证据链只是比我上一条说的更干净:你引的 243 是一个真实的、已提交的计数。

顺带,两条我查过之后决定不立卡

既然重测了这棵树,把另外两条一并了结,免得留成悬案:

  • unreasoned / miscounted 这两个字段名是复制残留 —— dropped-refinements.ts:466unreasoned 实际判的是 entry.sites.length === 0(兄弟文件 unemitted-schemas.ts:212 判的才是 entry.reason.trim() === '')。但 build-schemas.ts:3511 打给人看的那句话是 "carry an empty `sites` list" —— 面向用户的字串全是对的,残留只停在文件内部的标识符上。没有任何人会被它误导,不够一张卡。
  • reader 静默接受多余键 —— 你标为 out-of-scope 的那条。重测后我不立卡,理由是三条叠起来它已经不咬人了:committed 的 243 条键集全是 {sites},零实例;fix(spec): the dropped-refinement ledger refusal names sites, the key it requires #18816 落地后 docblock 已经明写这个 ledger 没有 reason 字段,诱因被你自己拆掉了;而兄弟 reader readUnemittedBaseline(:237-241)同样不拒绝多余键,两边对称,不存在「一边严一边松」的落差。真要加未知键拒绝,那是给两个 ledger 一起加的新门禁行为,是维护者的决定,不是这张卡的尾巴。

如果你不同意第二条的收口 —— 特别是你手上有我没测到的、真写过多余键的实例 —— 在这条下面说,我重开。


(本条已编辑一次:首发版本在「重新测(…)」那行里原样漏出了一个未被替换的 NOW 占位符(双花括号那种)。原因是我的发帖闸门调 post-stamped --dry-run 时没带 --comment= 目标,该工具因此在算 verbatim 计数之前就先退出了,闸门把「没打印计数」默认成了 0 并放行。闸门已修:缺计数现在按拒绝处理。取数时刻已改为实测值,正文其余部分未动。

另:修好后我重跑闸门,而闸门的最后一步是真的发帖而非只做 dry-run,于是多出了一条内容重复的 5722364389 —— 那条已被我改成一行指回本条的说明。本条是唯一的正文。)


Generated by Claude Code

os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

(这条是我发帖时的重复件 —— 我修正上面那条时重跑了发帖闸门,而闸门的最后一步是真的发帖而非只做 dry-run。正文见 5722349441;本条内容已移除,只留这行指路。平台不提供删除接口,所以它只能以这个形状留在串里。)


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 17, 2026 23:11
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit a84da60 Sep 17, 2026
41 checks passed
@os-bill
os-bill deleted the claude/issue-18747-dropped-refinements-error-shape branch September 17, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants