Uh oh!
There was an error while loading. Please reload this page.
Attribute a version to its package across prose distance, and put content/docs behind the vendor-stamp gate - #14017
Conversation
…tent/docs behind the gate (#13981) `check:vendor-version-stamps` did not read `content/docs`, and adding the root alone was measured to be insufficient rather than merely weak: the one site that matters, a customer-facing attestation in `permissions/authentication.mdx`, still landed UNATTRIBUTED because the package name sits ~90 characters back across a wrapped prose line, past the character window attribution used. Attribution is now two rules, neither of them a width: a name reaches to the end of its SENTENCE (a blank line, a bullet, a heading, a JSX tag, a `.` or a `;` outside a parenthetical), and the NEAREST CLAIMANT wins — every package in the tree may claim, with the vocabulary read from `pnpm-lock.yaml`. Widening the old window instead was swept and rejected with numbers: at gap 120 it attributes 23 more sites and binds `better-call@1.3.7`, `@better-auth/utils@0.4.2`, `minimatch 10.2.3` and an internal `'0.0.0-polyfill'` sentinel to a watched family member none of them is about, and at 200 it reds a permanently true historical sentence. `--attribution-sweep` prints both mechanisms side by side. Attribution alone did not separate the docs population's two buckets, so the classifier gains one shape: an UNANCHORED MEASUREMENT — "measured on 1.7.1" resting a standing claim on a reading taken against a version that no longer installs, with nothing saying when. The rule is POSITIONAL, because a sentence-wide substring test reds "Measured on the configuration the range *does* govern (…), 1.7.1 behaves identically" — where the verb governs a configuration, not the version. The gate found six stamps the old attribution could not see. All six are repaired the way the gate itself teaches — the sentence is scoped or anchored, never restamped, so no measurement is manufactured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fb32cc2d53059778231419ef60ca527171129529 && git checkout fb32cc2d53059778231419ef60ca527171129529
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 62a137baecb75d03c028779ef5c2ed0eacad8399 17c431ed9b4c5b01476417829e0ffcf6cb2f0d19 && git checkout -B drift-repro 62a137baecb75d03c028779ef5c2ed0eacad8399 && git merge --no-ff 17c431ed9b4c5b01476417829e0ffcf6cb2f0d19
node scripts/docs-audit/affected-docs.mjs --json 62a137baecb75d03c028779ef5c2ed0eacad8399 |
ACCEPT — ⛔ First: the scope question is MY defect, not the dev'sThe dispatch order said touching a Ruling: A — keep it.
⭐ And I verified the acceptance test directly in the diff: ⭐⭐ Zone 2 B — FALSIFIED, and the honest answer made the work BIGGERI hypothesised the existing phrasing test would already separate the two buckets once attribution reached them, which would have meant attribution alone, classifier untouched — the smallest possible delivery. Measured:
⇒ The same verdict for both. ⭐ I set that hypothesis up so that falsifying me would shrink the delivery. The measurement went the other way and the dev reported it that way rather than taking the cheaper path I had pre-blessed. ⭐⭐ Zone 2 A — CONFIRMED, and with a mechanism rather than a countWidening the window is not merely noisy; it produces the Zone 2 C — census unmoved, with a correction that matters15 sites over 405 files, identical to the card. ⇒ ⭐ With the exclusion applied, the in-scope docs population is EXACTLY ONE SITE — Blast radius — and a reverted refinement that shows the right instinct250 sites; 23 rows change; 6 become failures and every one is a true instance; 17 change attribution without changing a verdict, of which 5 are FIXES of existing misattributions (the pnpm override keys were binding a version to the left side of the 2 misattributions remain, both harmless ( ⭐ A stronger span rule that removed both was implemented and then reverted: it cost 1 genuine live-stale catch. The dev's reasoning, which I endorse: "The failing set is what the gate decides, so that trade is backwards." ⇒ Trading an enforced catch for two unenforced tidy-ups is a net loss, and recognising that is worth more than the refinement would have been. ⭐⭐ The dev caught its own VACUOUS pinOn ablation 3, the first version of the pin passed — but "it passed because the site was unattributed, i.e. for a reason unrelated to its phrasing." The pin was rewritten to force attribution on, and the reported reading is from the rewritten pin. ⇒ That is the negative-control discipline applied to its own test, unprompted: a pin that passes for the wrong reason measures nothing. Same class of catch as PR #13982's The Docs Drift bot advisory — answered by measurementThe bot flagged that 2 changed Verification I could and could not do
Governed-surface checkDiff: 7 files — The two out-of-scope items
Generated by Claude Code |
状态 —— 已复核并 ACCEPT;CI 全绿(33 项,零挂起零失败,按 check 名取最新一次);
⛔ 不绕行:本车道实测 REST ⇒ 间隔重试。本轮实测解除窗口 ~18 分钟(与既往 18–48 分钟一致)。姊妹 PR #14009 / #14012 已武装入队,本 PR 是本轮最后一个。 Generated by Claude Code |
更正上一条状态 —— auto-merge 是维护者手动挂的,⛔ 不是本席武装的。 上一条评论说本 PR「阻塞在 MCP 写入限流、间隔重试中」。该状态已作废:维护者在 出处:维护者当面指令,原话 「14017 我点了 auto merge」(本会话,2026-09-01)。
⇒ 记录在案:
治理面复核结论不变:diff 未触 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13981
check:vendor-version-stampsdid not readcontent/docs, and the card's ruling was thatadding the root alone is measurably insufficient. It is. Wiring
contentintoROOTSandstopping there leaves
content/docs/permissions/authentication.mdx:853unattributed —counted, never judged — because the package name sits about 90 characters back across a
wrapped prose line, past the character window attribution used. The work here is
attribution across prose distance, and the root is the smaller half of it.
The acceptance test: two buckets, one file
authentication.mdx:853— "…measured on 1.7.1"^1.7.2)unanchored-measurementauthentication.mdx:1217— "the stable 1.7.0 / 1.7.1 releases renamed it back toaccountId"Both directions are pinned in
--self-test, and the frozen-history pin judges its siteswith the attribution forced on — left to itself that sentence is unattributed, so the
pin would otherwise pass for a reason that has nothing to do with its phrasing. Ablations
below show each pin failing when its mechanism is removed.
What changed in the detector
1. Attribution is two rules, neither of them a width.
heading, a JSX tag, a
.or a;— whether that falls at character 20 or character 200.In code that is the statement terminator, so an attribution cannot leap between
statements. A terminator inside a parenthetical that closes does not end the sentence:
a running bracket depth cannot express that, and the first draft proved it —
it('…', () =>leavesa round bracket open for a whole test, and every terminator in the body was suppressed.
from
pnpm-lock.yamlrather than hand-listed.`better-call@1.3.7`andminimatch 10.2.3bind their own versions at any distance, and a pnpm override key bindsits version to the right side of the
>.2. One new red shape:
unanchored-measurement. A sentence resting a standing claim on areading taken against a version that no longer installs, with nothing saying when. The rule
is positional — the verb must govern the number — because a sentence-wide substring test
reds
packages/cli/src/commands/init.ts:108, "Measured on the configuration the range*does* govern (…), 1.7.1 behaves identically on
better-sqlite313.0.3", where the verbgoverns a configuration and not the version. The remedy is the same anchor both red shapes
are missing, so it is the same sentence either way.
3.
content/docsas a root, withcontent/docs/releasesexcluded by construction —release pages are written centrally and never edited by a code PR (CLAUDE.md), so a red
there names no author allowed to act on it. A configured exclusion that stops matching a
real directory is a hard error, not a silent no-op.
Zone 2, measured
A — widening
CLAIM_GAPis not the fix. Confirmed, with a mechanism.--attribution-sweepruns both mechanisms over the swept population and adds a CONTESTED column: attributions
binding a version whose nearest claimant is some other package.
A wider window does not see further, it sees more indiscriminately, because it models only
one claimant. Measured on the pre-repair corpus, gap 120 newly attributed 23 sites and among
them bound
`better-call@1.3.7`,`@better-auth/utils@0.4.2`,minimatch 10.2.3and an internal
'0.0.0-polyfill'sentinel to a watched family member none of them is about.And the cost is not only noise: at gap 200 the legacy mechanism reds a permanently true
historical sentence —
auth-manager.ts:3125, "That bridge dates from 1.6.20, where@better-auth/ssohardcoded the model" — by reaching back past a sentence boundary andpicking up "installed" from the next sentence. That is exactly the ":1217 direction": a red
whose only available repair turns a true sentence into a false one.
B — the phrasing test does NOT already separate the buckets. Falsified, in the expensive
direction. Given the attribution it lacked, the shipped classifier calls
:853and:1217the same thing:
:853says "measured on", which is none of the live-reading markers, so attribution aloneleaves the site the card says must be caught sitting green. The classifier had to gain the
measurement shape; the delivery is bigger than hypothesis B hoped, and this is the number
that says so.
C — the census has not moved. Re-derived on
origin/mainwith the shipped detector:15 sites over 405 files under
content/docs— 11 unattributed, 4 historical, 0 live-stale,identical to the card. One correction: all four historical sites live under
content/docs/releases, not three of four. With the exclusion applied, the docs populationin scope is exactly one site —
:853— so this root's whole enforced surface today isthe site it was added to judge.
D — blast radius over the whole swept population. 250 sites (249 code + 1 docs), 23 rows
changed against the shipped detector. Six became failures; every one is a true instance of
the class that the old attribution could not see, three from attribution alone and two more
from the new classifier shape, plus
:853:organization-add-member.ts:10auth-plugin.ts:2559;inside a parentheticalcli/test/init.test.ts:392admin-has-permission-endpoint.ts:109auth-manager.ts:2105authentication.mdx:853The other 17 rows change attribution without changing a verdict. Five are fixes of
existing misattributions: the override keys
'A>B': '0.5.0'ininit.ts:171-174andinit.test.ts:399were binding@better-auth/utils's version to the left side of the>,and
auth-manager.ts:2220was reading the ObjectStack release15.1.0as a better-authversion. Two known misattributions remain, both harmless (
historical, never enforced)and both reported rather than absorbed:
init.ts:334reads1.4.0(better-call's) asbetter-auth's in "better-auth itself depends on 1.4.0", and
template-consistency.test.ts:523reads0.5.0(@better-auth/utils's) as@better-auth/sso's across four words of prose. Reaching further to catch them wasimplemented and then reverted: matching any package-shaped name anywhere in the span
removed five misattributions that were all already
historical, and cost one genuinelive-stalecatch. The failing set is what this gate decides; silence bought with a real redis not a saving.
The six repairs, and the scope call on the docs edit
Every failure is repaired the way the gate itself teaches — remedy (b): scope or anchor
the sentence. Nothing is restamped, no measurement is re-run, no claim changes. Five are
comment-only edits in
packages/**(git diffconfirms every changed line there is inside acomment). "the installed 1.7.1" becomes "the then-installed 1.7.1"; "Measured on better-auth
1.7.1" becomes "Measured on the then-installed better-auth 1.7.1";
init.test.tsgains theanchor
#3653that its own test title already names.content/docsfile, and that is a scope call worth flagging. Thedispatch asks for a docs FILE edit to be reported before it is made, and there was no way to
ask mid-run. The alternative was to land a gate that reds on
main, which is worse foreveryone. The change is three words —
— plus a re-wrap of the paragraph it sits in. It is accurate: 1.7.1 was the installed version
when the measurement was taken. Revert it and the gate goes red on that one site, which is
the honest state of the trade; it is a small revert if the maintainer wants the sentence
handled differently.
Verification
--self-test: 33 assertions before, 64 after. New pins cover both buckets, sentencescope (full stop, blank line, bullet, parenthetical
;, unclosed bracket), nearestclaimant (specifier, prose adjacency, override key, and that an ordinary word is not a
claimant), the positional measurement rule and its scoped/dated/current neighbours, and
that the releases exclusion is declared and still names a real directory.
byte-identical blob (
git hash-objectequal to theHEADblob):attributes across a wrapped line";
renamedlive marker ⇒ all three "frozen history is never reported, evenattributed" pins fail. (The first version of that pin did not fail this ablation, which
is how the weakness was found and fixed.)
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no hand-built path list): 46 families,
comm -23empty, run at17c431ed9— the finalcommit. 43 green. Three are PREREQUISITE NOT MET, not red, and read as NOT MEASURED:
check:skill-examples(needs@objectstack/client-reactbuilt — a 38-package closure),check:dual-build-cjs-loadsandcheck:type-check-debt(both need every package'sdist).CI builds the farm and runs them.
check:entry-guardis in the set and green.pnpm eslint . --no-inline-config: 5,837 files, 0 errors, 0 warnings — the repo-widerun, so no narrowing to justify.
check:nul-bytesgreen;grep -naPover the diff's files finds no raw control bytes.pnpm --filter @objectstack/cli exec vitest run test/init.test.ts: 55 passed.docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md: none touched.Changeset
skip-changeset, applied additively and read back. This PR releases nothing: the onlynon-comment code change is
scripts/check-vendor-version-stamps.mjs, which no package'sfilesfield publishes; the rest is comments, one test comment andcontent/. That isroute 2 of the gate's own message, verbatim: "It releases nothing (.github/, .claude/,
skills/, docs/, content/, examples/, tests-only, and the like) -- apply the 'skip-changeset'
label."
The gate's own dated readings were refreshed
This gate exists to stop sentences that freeze a live value, so its docblock is held to the
same rule: the census (250 sites, 138 drifted, 51 anchored) and the window sweep (199 at
width 1, 250 at 4, 349 at 20) are re-measured and re-dated rather than left as they were.
Generated by Claude Code