feat(spec): check what an objectui read-point anchor's cited line SAYS, not only its sha label - #16800
Conversation
…S, not only its sha label `check:objectui-pin-citations` verified the sha spelling beside each read-point citation. A record is a claim about a LINE'S CONTENT, though, so an anchor that was wrong the day it was written — in a file byte-identical across a pin hop — survived every sha refresh and every line-number refresh. Measured twice: two such anchors in one re-read, three in the next. Adds a content assertion an author may write beside an anchor — the quoted first non-blank line of the cited range — which the gate checks against objectui at the pin. On a mismatch it searches the file and NAMES the line the quote really sits on, so an ordinary pin bump is a line-number edit and the quote never moves; a diff that rewrites a quote is therefore a reviewable claim that the read point changed. The ordinary run does not require ../objectui (absent in CI and in sibling worktrees) and never skips silently: it prints how many assertions it could not verify. `--verify-anchors`, the pin-bump mode, goes RED when it cannot read the pin, and prints the remaining coverage worklist — generated, never a roster. Coverage is a ratchet, not a migration: ASSERTED_ANCHOR_FLOOR is an equality in both directions, and no `gen:` fills a quote, because a quote copied without a re-read reproduces this defect at scale. Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude <noreply@anthropic.com>
…chor-content-assertions
📓 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): |
ACCEPT — head
|
| claim | measured |
|---|---|
one file, packages/spec/scripts/** |
✅ check-objectui-pin-citations.ts, +759/−6 |
Clause-②: no (path limb) |
✅ 0 files under packages/spec/src/** |
| governed surface | ✅ 0 files |
content/docs/releases/** |
✅ 0 files |
skip-changeset justified |
✅ files[] = dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json — nothing reaches scripts/ |
The declaration limb is also clear: the change tightens a gate rather than widening an accept set or a public surface, and packages/spec/scripts/** ships in no tarball.
Why the delivered thing is better than the thing the card asked for
The card was three wrong anchors. Those were already fixed by PR #15554, so a literal reading would have produced an empty PR. What landed instead makes the class detectable: the gate now checks what the cited line says, and on a mismatch it searches the file and names the line the quoted text really sits on. That is the difference between "anchor wrong since written" being invisible and being named.
⭐ The refusal is the part worth recording. 87 anchors could carry assertions and the dev did not bulk-fill them, on an argument I accept: a quote copied without re-reading reproduces this card's exact defect at scale, and ⛔ no mechanical check can distinguish a re-read from a careful copy. It says so in the gate header instead of claiming the design solves it. What the design does instead is remove the routine reason to touch a quote (an ordinary pin bump becomes a line-number edit), make the remaining case loud (rewriting a quote is a reviewable prose claim), and check the quote when it is written, against the real tree.
⭐ Proven by ablation against the real historical defect, not a synthetic one: PR #15554's actual wrong anchor, button.tsx:70-87 (which that rework corrected to :85-102). Leg 1 truthful → exit 0. Leg 2 same quote, anchor moved back → exit 1, That line IS in the file - at :85, not :70. Leg 3 bare button.tsx → exit 1, ambiguous across two real files, refused rather than guessed. Leg 4 coverage added without raising the floor → exit 1. Restore proved by blob hash and an empty git diff HEAD, not by an exit code.
PM ruling — the 87-anchor coverage migration: option A
Let coverage grow at pin bumps; ⛔ no card filed now. Reasons, in order:
- The re-read is only cheap when someone already has the objectui tree open at a known pin — and that moment is the pin bump. B and C both buy the re-read at the moment nobody has that tree open, which is exactly when copy-without-reading is most tempting.
--verify-anchorsgenerates the worklist on demand, so this is not tracking state living outside GitHub — there is nothing to go stale.- B additionally moves the work into
packages/spec/src/**, tripping the Clause-② path limb and putting pure measurement work on the contract-review tier.
ASSERTED_ANCHOR_FLOOR stands at 0, so nothing forces coverage to grow and it could sit there indefinitely. I judge that acceptable because the gate's value does not depend on the backlog — it binds every anchor written from now on, at the moment it is written. ⇒ This seat will revisit if the next objectui pin bump lands without moving the floor; if it does, #16524-style dispatch of component.zod.ts alone (28 anchors) is the smallest honest unit, as the dev proposed.
Second question — fold in #15604? Ruled no, agreeing with the dev's own reasoning: it could not argue from measurement that the two change the same criterion, and it showed ROOT_FILE_WATCH_HINTS / ROOT_DIR_WATCH_HINTS byte-identical with check:watch-hint-literal passing unchanged. ⛔ Merging them on a hunch is what produces the unreviewable PR.
Actions
ACCEPT → ready → enqueue. Not governed (Governed Surface Queue Guard green), so it is queue-eligible. Fixes #15618 closes the card on merge; ⛔ this seat does not merge by hand and does not bypass the queue.
Two observations carried, neither blocking: button.tsx resolves to two files in objectui at the pin (the new check refuses the ambiguity rather than guessing — leg 3), and check:pm-dispatch-gates needs >15 minutes for its own 1552-case self-test, exceeding this container's foreground cap twice before passing.
https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Generated by Claude Code
Fixes #15618
check:objectui-pin-citationsverifies the sha spelling beside each objectui read-pointcitation, and it does that correctly. A read-point record is a claim about a line's CONTENT,
though, and the sha is only how a reader knows which tree to count that line in. So an anchor
that was wrong the day it was written — in a file byte-identical across a pin hop — survived
every sha refresh and every line-number refresh. Measured twice: #10274 re-read four records and
found two such anchors; the rework of PR #15554 re-read eight and found three more.
This adds the missing criterion. It is not a fix for those anchors (already corrected in
#15554) and not a repair of a broken gate — the gate does what it declares; the finding is
that what it declares is not enough.
The mechanism: a quoted line, not a range hash
Beside an anchor an author may quote the first non-blank line of the cited range, and the
gate checks that quote against objectui at the pin:
A range hash was the other candidate and is rejected: a hash is opaque, so a reader cannot see
what it asserts, and a hash that is stable but wrong teaches nothing — it would re-create this
defect one level up. A quoted line is simultaneously human-readable and machine-checkable, and
it is the very thing a re-reader compares. The card's finding is that the record's claim is
about content while the check is on the label; a quoted line puts the content into the label.
The path may be omitted to continue the nearest preceding anchor, because that is how the
records already write a second range in the same file. The quote takes one to three backticks
(Markdown's own rule), so a cited line containing a backtick — a template literal, and these are
TSX files — is still spellable.
How this resists a rewrite that never re-read anything
⛔ No mechanical check can tell a genuine re-read from a careful rewrite, and this one does not
claim to: a script that copied whatever sits at the cited line would produce an assertion that
verifies. What the design does is remove the routine reason to touch a quote and make the
remaining ones loud.
quoted text really sits on. So the ordinary motion of a pin bump is a line-number edit, and
the quote never moves.
rare, and reviewable as prose. Before this, a bump rewrote line numbers everywhere and nothing
stood out.
who mis-copies is told immediately.
gen:fills any of this. The gate stays NO_GENERATOR in thecheck:generatedledger,now for a second reason: a command that grants coverage is exactly what must be earned by
measurement.
Cross-repo availability is a verdict, not a detail
../objectuiis absent in CI (lint.ymlruns this gate on a checkout of this repo alone) and insibling worktrees. Requiring it would make the gate unrunnable — a worse failure than the one
being closed. So:
assertions it could not verify, so a green run always states how much it measured;
--verify-anchors(the pin-bump mode) goes RED when it cannot read the pin, becausethere unavailability defeats the entire purpose.
Measured, all four combinations:
The population, and why this is a ratchet rather than a migration
Counted on this tree at pin
a472b0716:file:lineanchors inpackages/spec/src:NNNcontinuation anchors (inherit the preceding path).objectui-shacitationFilling 87 quotes at once would be a migration, not a gate change, and a quote copied without
a re-read reproduces this card's exact defect at scale — strictly worse than the gap. So coverage
is a ratchet:
ASSERTED_ANCHOR_FLOORis an equality enforced in both directions (below it,coverage was deleted; above it, raise it in the same PR so a later deletion cannot hide under
slack), counted from source text so it holds in CI where nothing can be verified.
It stands at 0 because this PR deliberately does not touch
packages/spec/src— the dispatchfenced that path, and adding an assertion to an existing record is a re-read, which is
measurement.
--verify-anchorsprints the remaining worklist, generated, never a roster:⛔ The citations and the anchors stay discovered by regex (
sourceFiles(SRC)+scanAnchors).There is no list of asserting files anywhere in the gate.
Evidence that the new assertion can FAIL
The self-test is registered ahead of the real run (
--self-test && ...,packages/spec/package.json:273)and gained 42 assertions across 20 cases: the spelling in full / continuation / wrapped / backticked-line forms; an
unquoted quote and an orphan continuation both FAIL; a wrong anchor beside a right quote fails
with the line the quote really sits on; an ambiguous path is refused rather than guessed; a
blank opening line is skipped and an all-blank range fails; the coverage floor equals the live
count; and a live round-trip against the real objectui pin, built from whatever the tree
holds so it pins no line of another repo and cannot rot — skipped loudly, never silently, when
the checkout is absent.
Beyond the fixtures, an end-to-end run against the real tree, using the actual wrong anchor
from #15554's rework (
button.tsx:70-87, which that rework corrected to:85-102). A temporaryrecord edit, proved on disk, then restored and proved restored by blob hash:
Leg 2 is the card's class, reproduced: the file is byte-identical across the pin hop, so no sha
refresh and no line-number refresh could ever have surfaced it — and the gate now names the true
line.
Gates
Derived, never guessed, on the merged tree (
c15785611, changeset = 1 path, three-dot vs mergebase
ed7243d52):50 of 51 exit 0, and
--ranreconciles:51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASUREDby the tool's own arithmetic. Also green:pnpm --filter @objectstack/spec typecheck(
tsc --noEmit+check:scripts-typecheck+check:test-typecheck,VERDICT command-exit 0under
scripts/pm/os-verify-lock.sh), the closure build, andeslinton the changed file.One is NOT MEASURED by my own reading — neither a pass nor a fail:
pnpm check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET: this gate reads built output, and some package has no dist/(87 packages). It needs a whole-repopnpm build, which is CI'srun. It also cannot be moved by this diff: no package source, no manifest and no
dist/bytechanges here, and
packages/spec/scripts/**ships in no tarball (measured below).pnpm check:pm-dispatch-gatesneeded three attempts — its own self-test suite outruns thiscontainer's foreground cap (killed at 540s, then at 900s, both making steady progress). Given a
2700s budget it passes:
✓ dispatch-gates self-test: 1552 cases pass.(exit 0). Recorded herebecause a run killed by a timeout is NOT MEASURED, never a pass, and the first two were.
Changeset
skip-changeset, measured rather than assumed:packages/spec'sfiles[]isdist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json, andnpm pack --dry-runputs 2073 files in the tarball(the positive control) of which 0 are under
scripts/. Nothing published moves.验收备注 / Acceptance notes
button.tsxas written in thecomponent.zod.ts/component.test.tsrecords resolves to two files in objectui at this pin
(
packages/components/src/renderers/form/button.tsxandpackages/components/src/ui/button.tsx).The record means the first; a reader cannot tell from the anchor alone. Not a defect in shipped
behaviour and not this card's scope; the successor is the coverage migration, which must spell
those paths further to satisfy the new check (the gate refuses the ambiguity rather than
guessing, LEG 3 above).
check:pm-dispatch-gatestakes longer than 15 minutes to run its ownself-test suite. An observation about a tool's runtime, with no reproducible defect behind it;
the successor is whoever next has to run that family locally.
measurement job, not a bulk fill, and the worklist is generated by
--verify-anchorsratherthan needing a card to hold it.
🤖 Generated with Claude Code
https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Generated by Claude Code