Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time by os-project-manager · Pull Request #13979 · objectstack-ai/objectstack · GitHub
Skip to content

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time - #13979

Merged
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift
Aug 31, 2026
Merged

Hold vendor version stamps to a shape that survives the pin moving, instead of sweeping them a fifth time#13979
os-project-manager merged 6 commits into
mainfrom
claude/issue-13940-better-auth-stamp-drift

Conversation

@os-project-manager

@os-project-manageros-project-manager commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #13940.

Part of, not a closing keyword — deliberately, and stated as the dispatch asked. The
producer of this class is shut here; the card's 93-line population is not restamped, and
must not be. 130 stamps still name a version other than the resolved one, by design: they
are anchored historical facts, and rewriting them without re-measuring is the one outcome
worse than leaving them. Whether that closes the card is the PM's triage call, not this
PR's.

What the recurrence actually is

Four hand sweeps (29 stamps, +3, +2, now 93) with the population growing means the
sweep was trailing a producer rather than converging on a remainder. The producer is not
"stamps name an old version". A stamp naming an old version is frequently true:

// [#11374] Bound from better-auth 1.7.1's own MySQL schema: a unique ...

That was true when written, is true now, and will be true after 1.9.0. Nothing rots.

The producer is the other shape — a sentence fusing a permanent fact with a live
reading
of a value that moves:

// still true of the installed stable `@better-auth/scim@1.7.1`

the installed is a present-tense claim about this tree. When the pin moves, that sentence
is not stale, it is false. Every bump falsifies every live-reading stamp at once, and
only a person reading them can tell. That is why the remedy kept being a person.

This is the same defect PR #13962 repaired one level up, in a docblock that froze a reading
of cases.length. Its remedy is this one: stop freezing a live value; anchor the fact to
when it was true.

Zone 2 A — the triage, before touching anything

The card's hypothesis that the 93 are "not one kind" holds. Measured by --json on this
branch (249 stamp sites, better-auth resolved to 1.7.2 from pnpm-lock.yaml):

bucketcountwhat it isaction
historical130names a non-resolved version but makes no claim about this tree — carries a date, an issue reference, or is a plain fact about a named releaseleft alone. Permanently true; green at every future bump
unattributed114a version the gate cannot attribute to a package it resolves (better-sqlite3 13.0.3, @better-auth/utils 0.4.2, peer ranges)counted, never judged
live-stale18 → 0a present-tense installedness claim naming a version that is not installed — false todayall 18 repaired (below)
current2names the resolved 1.7.2re-verified, see below
fixture3synthetic test input, not an attestationdeclared, not rewritten

The third bucket the card predicted — "may have changed at 1.7.2, needs a real check" — is
real but is not separable by reading. Distinguishing "still true on 1.7.2" from "changed
underneath" requires re-running each measurement against the vendor. That is the 130, and
this PR deliberately does not guess at them.

What was done per bucket, and why each restamp is honest

18 live-stale sites repaired. 16 by scoping, 2 by re-verification.

Scoping (16).the installed better-auth 1.7.1 becomes the then-installed better-auth 1.7.1. This re-measures nothing and claims nothing new: it removes a false present-tense
assertion and leaves exactly the measurement that was actually made. Honest because the
sentence now says only what someone actually observed, and true for good.

Re-verification (2). These two were genuinely re-measured against the installed 1.7.2
today, so restamping them attests to a measurement that was really taken:

  • packages/cli/src/commands/init.ts — read off the installed manifest:
    @better-auth/scim@1.7.2 peers @better-auth/utils at exactly 0.4.2 and better-call
    at exactly 1.4.0, unchanged from 1.7.1. Restamped to 1.7.2and dated.
  • packages/plugins/plugin-auth/src/auth-schema-config.tsSCIMOptions in the installed
    @better-auth/scim@1.7.2dist/index.d.mts declares six members (connections,
    authentication, managedConnections, identity, projection, compatibility) and no
    schema / modelName / fields. Enumerated with a positive control (the block really
    was captured: 18 lines, 6 members found). Restamped to 1.7.2and dated.

Every other stamp keeps its version. No stamp was restamped without a measurement.

Zone 2 B — the mechanism, measured

The obvious gate was designed, measured against the real corpus, and rejected. "Hold
every version stamp equal to the resolved pin" cannot be built honestly here:

  • it would force 130 anchored historical facts to be rewritten into measurements nobody
    took;
  • three of the sites are fixture data, not attestations — a synthetic npm registry
    payload inside check-prerelease-pin-watch.mjs whose version is arbitrary input proving
    the watcher notices a stable release. A pin-equality sweep corrupts another gate's test
    input.

So the shipped gate holds phrasing, not version equality:

  • RED — a live-reading stamp naming a version that is not the resolved one, with no date
    scoping the present tense. Today: 0.
  • GREEN — an anchored or plainly historical stamp naming any version.
  • REPORTED, never enforced — the drift census (130), so a bump author can see the
    surface without being handed a remedy that manufactures claims.

The consequence is the whole point: once a stamp is anchored it stays green at every
future bump.
The per-bump population falls from "every stamp naming the old version"
(unbounded, 29 → 93) to "stamps written as live readings", which the gate holds at zero.

scripts/check-vendor-version-stamps.mjs — 5,590 files, no build, no install (the pin
resolves statically from pnpm-lock.yaml), 33 self-test assertions, wired into lint.yml.

It matches prose and specifier spellings, across all five packages, which the card
required: #10188 recorded that a better-auth@-only comment-vs-pin gate misses prose, and
on this corpus the specifier spelling is the minority. Detection is co-occurrence in a
window (the check-corpus-claim-drift.mjs design), so better-auth 1.7.1,
`@better-auth/sso@1.7.1`, better-auth (1.7.1) and versions split across a line break
are all reached.

Three findings worth recording, each from measuring rather than assuming:

  1. The window sweep has no plateau, and it does not matter. Site count climbs
    monotonically (198 at width 1, 249 at 4, 343 at 20) — a plateau was never going to exist.
    The failing set is 0 at every width from 1 to 20: the verdict is window-insensitive,
    because a live-reading claim and the version it names are always the same clause. The
    header says so instead of implying a calibration that was not performed.
  2. Attribution is the half a naive detector gets wrong. A first cut read
    @better-auth/utils@0.4.2 as a better-auth stamp (substring) and better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1 as three better-auth stamps. A package name
    now claims the first following version only, must stand alone, and ranges (^12.0.0) are
    not stamps at all.
  3. A package at two versions resolves to nothing, not to a guess.@better-auth/utils
    is genuinely in this tree at both 0.4.2 and 0.5.0, so a stamp saying 0.4.2 is right
    about one copy. Ambiguity makes it unjudgeable — a true answer; picking the highest is a
    false red.

Zone 2 D — are the gate-script stamps load-bearing?

One is, and it is the strongest argument against the pin-equality design.

Re-derived counts

The card measured 95 stamps / 56 files on a pre-merge branch. On origin/main at
aee1fd9ec the card's own command still reads 95. On this branch it reads 93 in 56
files
excluding this PR's new gate file — the two removed are the re-verified restamps.

Note the card's command now reads 129 including the gate file: 36 of those lines are the
gate's own documentation and fixtures. Stated so nobody reads the growth as new drift.

The content/docs/** population — counted, not fixed, and not edited

The card's command scopes to --include='*.ts' --include='*.mjs' --include='*.mts', so the
customer-facing copy of this defect was never in its population. No content/docs/** file
is touched by this PR.
It was measured, with the hits printed rather than only counted:

Running this gate's own detector (imported — the entry guard makes that safe) over 405
.md/.mdx files under content/docs: 15 stamp sites — 4 historical, 11
unattributed, 0 live-stale.

Two lines in one file are worth citing, because they are the triage buckets standing side by
side in customer-facing copy rather than a taxonomy invented for this PR:

  • content/docs/permissions/authentication.mdx:853"measured on 1.7.1, where addMember
    builds its endpoint with no path"
    . An attestation in exactly the sense this PR is
    about
    , shipped to readers, now naming a version that is not installed.
  • content/docs/permissions/authentication.mdx:1217"the stable 1.7.0 / 1.7.1
    releases renamed it back to accountId"
    . Correctly frozen: a statement about what
    those releases did. Rewriting it to 1.7.2 would make it false.

And the measurement found a limit in this gate worth stating plainly, because it changes
where the mechanism belongs.
Adding content/docs to ROOTS would not be sufficient:

  • :853 is seen but lands unattributedbetter-auth sits ~90 characters earlier, on the
    previous line, past CLAIM_GAP. Reported, never judged.
  • :1217 is not seen at all: no watched package name occurs within the window (measured:
    zero better-auth mentions in lines 1213-1221).

Prose spreads a package name and its version much further apart than code does, so a docs
lane needs a wider claim gap or MDX-aware attribution — a separate piece of work, with its
own false-positive budget over 405 files. That is an argument about where the mechanism
belongs and how far it reaches, not about whether one is affordable.

⇒ Recommended as its own card, not folded in here. content/docs/releases/** is
release-owned and fenced unconditionally by AGENTS.md, so any such card must exclude it by
construction.

Verification

  • pnpm check:vendor-version-stamps — its own printed verdict: OK — 5590 file(s) scanned,
    249 version stamp(s); resolved 1.7.2; 130 name(s) a different version. Self-test: 33 checks pass.
  • Ablation, from the committed state: one repaired stamp reverted on disk (confirmed by
    grep counts, 0 injected / 1 removed), gate went red with exactly 1 finding; restored
    via git checkout HEAD -- with an absolute path, restore proved byte-identical by
    git hash-object against the HEAD blob (e0df265d), and git diff HEAD clean.
  • Gates: 37 derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    at b4ec970c8, all 37 run; comm -23 derived-minus-run is empty.
    33 exit 0. Four exit
    non-zero as an explicit PREREQUISITE NOT METcheck:i18n, check:i18n-coverage,
    check:type-check-debt, check:dual-build-cjs-loads each print that they need a built
    closure and that nothing was measured; CI builds before running them. Recorded as NOT
    MEASURED, not as passes.
  • pnpm check:ratchet-remedy-authority run explicitly (it cannot be path-derived): green,
    181 scripts swept. This gate offers no ratchet, ledger or baseline, so it needs no
    authority marker and owes no CONTROL entry.
  • check:entry-guardcaught a real defect in the new gate — it exports its analysis
    functions and ran its top level on import. Now behind one isEntrypoint site; importing it
    is inert (proved: import prints only the export list).
  • pnpm exec eslint --no-inline-config on the 16 changed lintable files: 0 errors, 0
    warnings
    , count read from --format json (16 files). Repo-wide lint is CI's, and this
    narrowing is safe to declare: eslint.config.mjs:328 records no parserOptions.project
    and no typed rules, so this diff cannot move the verdict on any untouched file.
  • No package test run, declared: every changed line under packages/ is a comment except one
    ledger why: string, and both gates that parse that table (check:dispatcher-error-vocabulary,
    check:route-envelope) are green. eslint parsed all 16 edited files, so no syntax breakage.

Changeset

None, plus skip-changeset — precedent and the workflow's own prescription. lint.yml
calls a PR that edits a CI-internal script "the textbook skip-changeset case — such a PR
releases nothing". Every packages/ change here is a comment; nothing publishes. The four
most recent scripts-only gate commits on main carry zero changesets.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/runtime, touching 3 documentable anchor(s). ⚠️6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx(via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts, packages/plugins/plugin-auth/src/admin-revoke-user-session-match-guard.ts, packages/plugins/plugin-auth/src/adopt-membership.ts, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 43 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 eb649cb8bc3e17ac0302384eff90067f0f51b838packageMentionDocs.

Which tree this was computed on

This run read content/docs from 70f4ef316d73529bf9e805dbeabd3c7c948b845c — the merge of head b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 into base eb649cb8bc3e17ac0302384eff90067f0f51b838, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 70f4ef316d73529bf9e805dbeabd3c7c948b845c && git checkout 70f4ef316d73529bf9e805dbeabd3c7c948b845c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 b4ec970c875b0e2d22ad6d29192a79c1a44a31b1 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff b4ec970c875b0e2d22ad6d29192a79c1a44a31b1
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb649cb8bc3e17ac0302384eff90067f0f51b838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actionsgithub-actionsBot added ci/cd dependencies Pull requests that update a dependency file tests labels Aug 31, 2026
@claude

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main and the branch, ⛔ not the shared checkout.

⛔ My mechanism hypothesis would have been a defect factory, and the report proves it with counts

I wrote Zone 2 B as "something that makes the next drift detectable — a gate deriving the installed version and checking stamps against it."A pin-equality gate cannot be built honestly here:

⇒ My hypothesis, implemented as stated, produces a false attestation 130 times and breaks a neighbouring gate. ⛔ It was not a near-miss.

⭐⭐ What it built instead — and the sentence that carries the whole design

The real producer is a sentence fusing a permanent fact with a LIVE READING of a moving value: "the installed better-auth 1.7.1" is not stale after a bump — it is FALSE.

The gate holds PHRASING, ⛔ not version equality. It reds on exactly that shape and nothing else; the 130-strong drift is measured and reported, never enforced.

The consequence is the class fix the card demanded: an anchored stamp stays green at every future bump, so the per-bump population falls from "every stamp naming the old version" — unbounded, and observed growing 29 → 95 across four hand sweeps — to "stamps written as live readings", held at zero.

⇒ That answers "why will this class not reappear" structurally. ⛔ A fifth sweep would not have.

Zone 1 rule 1 — held exactly, and the split is the proof

⛔ No stamp was restamped without a measurement. The 18 live-stale sites went to 0 by two different routes:

  • 16 by scoping"the then-installed X", which re-measures nothing and makes the sentence honest about being historical. ⭐ That is the third option my Zone 1 offered, chosen where re-verification was not affordable.
  • 2 by real re-verification against 1.7.2, restamped because the measurement was actually redone.

And on the bucket I predicted but could not name: "the card's predicted 'may have changed at 1.7.2' bucket is real but NOT separable by reading — it needs re-running each measurement upstream. That is the 130; I did not guess at them." ⇒ ⭐ Exactly the instruction: ⛔ if you cannot tell which bucket, leave it — do not guess.

Zone 2 D — my warning paid off, one of three

check-prerelease-pin-watch.mjs:943is load-bearing (the fixture above) and was left untouched. check-route-envelope.mjs:795's note is parsed, but vendorWireNoteConforms only asserts it contains vendor: / reader: / partner:, so the version is not load-bearing — checked rather than assumed. Two more are header prose.

Three measurements inside the gate's own design worth keeping

  1. The window sweep has no plateau (198 sites at width 1, 249 at 4, 343 at 20) — but the FAILING set is 0 at every width 1..20, so width is not a calibration, and the header says so rather than presenting a tuned number as a derived one.
  2. Attribution is the half a naive detector gets wrong: a first cut read @better-auth/utils@0.4.2 as a better-auth stamp by substring, and "better-auth 1.7.1 behaves identically on 13.0.3 and on 12.11.1" as three stamps.
  3. @better-auth/utils is genuinely installed at both 0.4.2 and 0.5.0, so it resolves to nothing rather than to a guess.

⭐ The gate family caught the dev's own bug

check:entry-guard found the new gate exporting its analysis functions while running its top level on import. Now behind one isEntrypoint site, with import proved inert. ⇒ The derived family did its job on a brand-new file, which is the best evidence that running the whole family is not ceremony.

The docs population — counted, ⛔ not touched, and the obvious fix measured as insufficient

0 content/docs files in the diff (verified). Using this PR's own detector over 405 files: 15 sites — 4 historical, 11 unattributed, 0 live-stale. Both lines I cited are confirmed as the two buckets side by side: :853"measured on 1.7.1" is a customer-facing attestation now naming a non-installed version; :1217 is correctly frozen.

⭐⭐ And the important measurement: adding content/docs to the gate's ROOTS would NOT suffice:853 lands unattributed (the package name sits ~90 chars earlier, past CLAIM_GAP=60) and :1217 is not seen at all. ⇒ "it would create the appearance of coverage without the substance" — the declared-not-enforced shape this repo treats as worse than no check. Filed separately per my ruling below.

Rulings on the two open questions

Q1 — does #13940 close here?B, as recommended. The class is shut and that was the card's ask; but "unverified" was the card's own stated failure direction and it survives this PR by design in the 130. ⛔ Option C would re-commission the sweep this lane just ruled against. ⇒ Card closes on this PR; the residue is filed as its own card, to be paid down per surface as each is next touched, ⛔ never as a batch.

Q2 — docs its own card?A, as recommended, and the measurement above is why: C is not merely weaker, it is measurably insufficient, and B understates a live customer-facing attestation. Filed, excluding content/docs/releases/**by construction — release-owned, and 3 of the 4 historical docs sites live there.

Gates

37 derived, 37 run, comm -23 empty. 33 exit 0; four exit non-zero as an explicit PREREQUISITE NOT MET printing "nothing was measured" — recorded NOT MEASURED, ⛔ never as passes. check:ratchet-remedy-authority run explicitly per #13813 — green, and the new gate offers no ratchet, ledger or baseline, so no authority marker is owed. Ablation from the committed state with the mutation proved on disk by grep counts and the restore proved byte-identical by blob hash.

⚠️ Recount discipline worth noting: the card's command reads 95/56 on origin/main and 93/56 on the branch excluding the new gate — ⭐ and the dev states that including the gate file it reads 129, because 36 of those lines are the gate's own docs and fixtures, "stated so nobody reads that as new drift."

Governed-surface check

Diff: one new scripts/** gate, .github/workflows/lint.yml, package.json, and comment-level edits across five packages. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.


Generated by Claude Code

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

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-project-manager@claude