Skip to content

feat(spec): check what an objectui read-point anchor's cited line SAYS, not only its sha label - #16800

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-15618-anchor-content-assertions
Sep 8, 2026
Merged

feat(spec): check what an objectui read-point anchor's cited line SAYS, not only its sha label#16800
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-15618-anchor-content-assertions

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15618

check:objectui-pin-citations verifies the sha spelling beside each objectui read-point
citation, 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:

`plugin-dashboard/src/index.tsx:204` first line
`{ name: 'icon', type: 'string', label: 'Icon (Lucide name)' },`

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.

  • On a mismatch the gate does not merely fail — it searches the file and names the line the
    quoted text really sits on
    . So the ordinary motion of a pin bump is a line-number edit, and
    the quote never moves.
  • Which makes a diff that rewrites a quote a claim that the read point's content changed:
    rare, and reviewable as prose. Before this, a bump rewrote line numbers everywhere and nothing
    stood out.
  • And the quote is checked at the moment it is written, against the real tree, so an author
    who mis-copies is told immediately.
  • ⛔ No gen: fills any of this. The gate stays NO_GENERATOR in the check:generated ledger,
    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

../objectui is absent in CI (lint.yml runs this gate on a checkout of this repo alone) and in
sibling worktrees. Requiring it would make the gate unrunnable — a worse failure than the one
being closed. So:

  • the ordinary run does not require the pin, and ⛔ never skips silently: it prints how many
    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, because
    there unavailability defeats the entire purpose.

Measured, all four combinations:

$ tsx scripts/check-objectui-pin-citations.ts                       # objectui present
EXIT=0
   0 anchor content assertion(s) verified against objectui at a472b0716; 122 file:line anchor(s) seen.

$ OBJECTUI_ROOT=/nope tsx scripts/check-objectui-pin-citations.ts   # the CI shape
EXIT=0
   WARNING 0 anchor content assertion(s) NOT VERIFIED - no git checkout at /nope; 122 anchor(s) seen.
   No anchor carries a quoted first line yet, so nothing here checks an anchor's CONTENT.

$ tsx scripts/check-objectui-pin-citations.ts --verify-anchors      # objectui present
EXIT=0   (prints the coverage worklist, below)

$ OBJECTUI_ROOT=/nope tsx ... --verify-anchors
EXIT=1   "cannot read objectui at the pin: no git checkout at /nope ... it is no run"

The population, and why this is a ratchet rather than a migration

Counted on this tree at pin a472b0716:

measure count
backticked file:line anchors in packages/spec/src 122
of those, resolving to exactly one file in objectui at the pin 87
bare :NNN continuation anchors (inherit the preceding path) 51
spec sources carrying a .objectui-sha citation 15
pin citations (12 asserting / 16 historical) 28
anchors carrying a content assertion today 0

Filling 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_FLOOR is 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 dispatch
fenced that path, and adding an assertion to an existing record is a re-read, which is
measurement. --verify-anchors prints the remaining worklist, generated, never a roster:

Anchor content coverage at a472b0716: 0 asserted, 87 objectui anchor(s) still carry no content assertion.
   28  packages/spec/src/ui/component.zod.ts
   14  packages/spec/src/ui/component.test.ts
    9  packages/spec/src/ui/view.zod.ts
    ... 17 more files

⛔ 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 temporary
record edit, proved on disk, then restored and proved restored by blob hash:

HEAD blobs: rec=3d939c5e3d25897cc9b9047a16d7d65fb690371c gate=e70e24972d69b14b628fa9966149288c4b37d0c4

LEG 1  truthful assertion, floor raised to 1
  on-disk: injected=1  original-anchor-occurrences=0  floor=1
  EXIT=0   1 anchor content assertion(s) verified against objectui at a472b0716

LEG 2  same quote, anchor moved to the historically wrong lines
  on-disk: ':70-87'=1  ':85-102'=0
  EXIT=1
  the asserted first line does not match .../form/button.tsx:70 at this pin.
        asserted: inputs: [
        actually: >
      That line IS in the file - at :85, not :70.
      Either the anchor drifted by 15 lines, or it was wrong when written.
      Re-READ before you re-point it.

LEG 3  ambiguous path spelling
  EXIT=1   `button.tsx` matches 2 files in objectui at this pin:
             packages/components/src/renderers/form/button.tsx
             packages/components/src/ui/button.tsx
           Spell enough of the path to name one.

LEG 4  coverage added without raising the floor
  EXIT=1   1 assertion(s) in packages/spec/src, ASSERTED_ANCHOR_FLOOR says 0.

RESTORE  rec hash 3d939c5e... == HEAD, gate hash e70e2497... == HEAD
         git diff HEAD --stat: []   git status --porcelain: []

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 merge
base ed7243d52):

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
  -> 51 command(s); reconciled with --ran

50 of 51 exit 0, and --ran reconciles: 51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASURED by the tool's own arithmetic. Also green: pnpm --filter @objectstack/spec typecheck
(tsc --noEmit + check:scripts-typecheck + check:test-typecheck, VERDICT command-exit 0
under scripts/pm/os-verify-lock.sh), the closure build, and eslint on 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-repo pnpm build, which is CI's
    run. It also cannot be moved by this diff: no package source, no manifest and no dist/ byte
    changes here, and packages/spec/scripts/** ships in no tarball (measured below).
    pnpm check:pm-dispatch-gates needed three attempts — its own self-test suite outruns this
    container'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 here
    because 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's files[] is
dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json, and npm pack --dry-run puts 2073 files in the tarball
(the positive control) of which 0 are under scripts/. Nothing published moves.

验收备注 / Acceptance notes

  • noted, not filedbutton.tsx as written in the component.zod.ts / component.test.ts
    records resolves to two files in objectui at this pin
    (packages/components/src/renderers/form/button.tsx and packages/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).
  • noted, not filedcheck:pm-dispatch-gates takes longer than 15 minutes to run its own
    self-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.
  • The 87-anchor coverage migration is reported to the PM, not filed: per the dispatch it is a
    measurement job, not a bulk fill, and the worklist is generated by --verify-anchors rather
    than needing a card to hold it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x


Generated by Claude Code

…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>
@github-actions github-actions Bot added the size/l label Sep 8, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

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 b38821d1ce220527e2a7f34e254a96c48e2a9ba3packageMentionDocs.

Copy link
Copy Markdown
Collaborator Author

ACCEPT — head c1578561, 41/41 checks green. Readying and enqueueing.

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x, 2026-09-08T06:40Z (clock read).

Enqueue reading from check runs, not an aggregate: all 41 runs on c157856111ed8e236041d60e33c6b11dd4633737 are completed, every conclusion success or skipped, none in_progress, none failed. ⛔ The check_suite.completed rollup was not read as the gate — it excludes cancelled suites and legacy statuses by its own terms.

Scope claims, re-measured in-seat against merge-base ed7243d52

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.jsonnothing 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:

  1. 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.
  2. --verify-anchors generates the worklist on demand, so this is not tracking state living outside GitHub — there is nothing to go stale.
  3. B additionally moves the work into packages/spec/src/**, tripping the Clause-② path limb and putting pure measurement work on the contract-review tier.

⚠️ The risk I am accepting, stated rather than glossed: 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants