Re-derive the tenant-audit write-call-site census as an in-tree artifact (page + generator + drift gate) - #13584
Conversation
The measurement that decides this repair family's severity lived in a comment on issue #13178. That issue became unreachable — 404 on unauthenticated REST, on the rendered page and on authenticated MCP alike, while its neighbours answer 200 — and took the census with it while three open cards still named it as their input. What survived did so by luck: a changeset author had quoted two figures in prose. Adds the isSystem census triple's shape for the tenant-audit control: scripts/tenant-audit-census.mjs the instrument content/docs/permissions/...mdx the page (prose + generated figures) docs/audits/...counts.md the per-site ledger, regenerated whole scripts/check-tenant-audit-census.mjs the gate It is a DRIFT gate, deliberately not an anchor gate: rows aggregate by (file, verb, object, tenancy, context posture) and carry no line numbers, so a pure displacement cannot move them. That skips the sibling's open defect class — anchors that rot on displacement and a repair arm that then misreads displacement as a population change. Measured: 214 write call sites, 143 statically decidable, 41 tenancy-enabled carrying no tenant context. The deviations from all four carried figures are recorded on the page untrimmed, including that the 135/77% "silenced by the isSystem guard" figure has no surviving corroboration and is not reproduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
⛔ The split is not possible — stopping rather than forcing it, per your standing offerYou asked me to confirm the trimmed branch still passes before splitting, and to say so with evidence if the one line turns out to be load-bearing. It is load-bearing, in both directions. Recommend parking this PR for human merge instead. What was measured
Direction A — the six-file PR alone (page present, governed line removed): Direction B — the one-line governed PR alone (governed line present, page absent): ⇒ The two halves are green only together. Splitting yields two PRs that are each red on a required gate, and the governed one stays red until the other merges — an ordering deadlock, not a sequencing inconvenience. Why there is no third option
⭐ The general shape, which is probably the more useful findingEvery PR that adds a genuinely new hand-written page under
⇒ modify a docs page → ordinary PR. Add one → human-merge-only. That distinction is invisible in the dispatch, invisible in the file list until the gate runs, and it is not written down anywhere I could find. State of this branch⛔ Nothing was pushed, split, trimmed or re-baselined. Say the word and I will do either: park this for human merge as-is, or relocate the page out of Generated by Claude Code |
…nant-audit-census-artifact
`packages/plugins/plugin-security/src/platform-admin-service.ts` landed on main between this branch's census run and CI's, so the committed artifacts named 533 scanned sources where the tree holds 534 — and the drift gate caught its own artifact within the hour of it being written. Population unchanged: the new source contains no engine write call site (no insert/update/delete call syntax at all; the census places 0 sites and 0 unresolved receivers in it). 214 / 143 / 41 all hold, so the page's "cite 41 / 214" redirection needs no edit. Only `Sources scanned` moves, in both artifacts, which is exactly the shape "+1 scanned source, +0 population" should take. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
`check-section-landing-index` holds each section's "What's in this module" block to its meta.json in both directions, and the merge brought that gate into this card's derived family. Adding the page to meta.json without a row here left the reader's map short by one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Regenerated against the merged tree — green on
|
| figure | before | after |
|---|---|---|
| Sources scanned | 533 | 534 |
| Write call sites | 214 | 214 |
| Object name statically decidable | 143 | 143 |
| Object name chosen at run time | 71 | 71 |
| Carrying no tenant context | 83 | 83 |
| …and decidably tenancy-enabled | 41 | 41 |
| Elevated / not elevated / undecidable | 46 / 8 / 77 | 46 / 8 / 77 |
| Placements: object name / name parameter / ledger | 19 / 15 / 11 | 19 / 15 / 11 |
| Engine-shaped types · declared objects · non-engine subtracted | 56 · 297 · 119 | 56 · 297 · 119 |
Nothing moved beyond the +1 source. The regenerated diff is literally two lines, both the same figure:
-Scanned 533 tracked non-test sources under `packages/services/` and
+Scanned 534 tracked non-test sources under `packages/services/` and
-| Sources scanned | 533 |
+| Sources scanned | 534 |
The headline figures the page tells downstream cards to cite — 214 / 143 / 41 — are unchanged, so the 41 / 214 redirection text needed no edit. Every recorded deviation stands exactly as written; the 135 / 77% figure stays un-reproduced.
Why the population held still, verified rather than assumed
Exactly one new non-test source entered the corpus roots: packages/plugins/plugin-security/src/platform-admin-service.ts. It contains no insert / update / delete call syntax at all, and the census places 0 sites and 0 unresolved receivers in it. So +1 scanned, +0 population is measured, not inferred — which is also a small check on the instrument: the scanned count and the population count move independently, as they should.
One more finding, caught by re-deriving after the merge
The derived gate family grew from 31 to 63 across the merge, and one of the newcomers was a real finding against this PR: check-section-landing-index — the census page was in permissions/meta.json but had no row in permissions/index.mdx's "What's in this module" block, which that gate holds to meta.json in both directions. Fixed by writing the row (link text and gloss are hand-written by design; the gate reads hrefs only).
⇒ this is the argument for re-deriving the family after a merge rather than reusing the dispatch list. Reusing it would have shipped this red.
Gate results on ed0df3846
Exit codes captured before any pipe. 61 of 63 green. The census gate's own verdicts:
✓ check-tenant-audit-census self-test: 10 cases pass
✓ check-tenant-audit-census: OK -- 214 write call sites certified (143 decidable,
41 tenancy-enabled with no tenant context), 15 prose figures held to the census
tenant-audit-census: … unresolved receivers 0
Two non-zero, both NOT MEASURED rather than findings — each says so itself:
check-test-completeness.mjsexit 3 — needs a savedturbo run testlog; the gate prints "Nothing was measured … ⛔ It is NOT a finding". Exit 3 is deliberately distinct from a finding's 1.check:type-check-debt—--re-measurerefuses without a full workspace build, and is derived here only because this PR editslint.yml.check:type-check-coverage, the same script without--re-measure, is green.
⛔ Nothing re-baselined. Still draft, not armed, not ready — parked for human merge as agreed.
Generated by Claude Code
|
| 02:35Z | red — Scanned 533 → 534. Regenerated, pushed, green. |
| 03:19Z | red again, ~20 minutes later — and this time the POPULATION moved: write call sites 214 → 215, without-tenant-context 83 → 84, undecidable 71 → 72, elevation-undecidable 77 → 79, decidably-not-elevated 8 → 6, plus 7 [prose-count] findings on the hand-written deviation rows. |
⇒ the dev's measured maintenance cost — "one unrelated source landing anywhere under packages/services/ or packages/plugins/ is sufficient to red this gate" — is not a worst case. On today's tree it is the normal case, at roughly 20-minute intervals.
Why that is a landing problem and not just a chore
- This PR is governed (
.claude/workflows/docs-accuracy-audit.js) ⇒ human merge only, ⛔ never armed, ⛔ never queued. - Human-merge latency is measured in hours. The drift interval is measured in minutes.
- ⛔ And the merge queue is not an escape, even setting the governed rule aside: the queue rebuilds the PR against current
mainand re-runs the checks — it does not regenerate the artifact. A stale census would be ejected there too.
⇒ the two requirements are in direct tension, and no amount of care on this branch resolves it: a freshly-regenerated push is green only until the next unrelated merge.
What a merger needs from this
Either merge within the window right after a regeneration push — the branch is green at that moment and the content has been reviewed — or say the shape should change, in which case the honest option is that the gate assert "this was true at commit X" against a recorded baseline sha rather than against live HEAD. ⭐ That is arguably what a dated census claims anyway.
⛔ This seat is not proposing that change and has not made it. The dev has been asked for its read on whether the artifact is landable as-is, and if the answer is "the design must change" it goes to the maintainer as a decision card, ⛔ not into this PR.
⛔ Nothing here is a defect in the work
The gate is doing precisely its job, and doing it well — it caught its own artifact going stale twice, within the hour, including once before CI had even finished. ⭐ A census that goes stale loudly is the entire point of the card. Every recorded deviation still stands as written; the 135/77% figure stays un-reproduced. The problem is a cadence mismatch between the gate and the merge regime, not the gate.
decidably not elevated went 8 → 6 — DOWN while the population went UP. That is not explained by "a file arrived", and the dev has been asked to establish the cause before regenerating past it. A classifier whose verdicts move without an obvious cause deserves more attention than the drift does.
Generated by Claude Code
…nant-audit-census-artifact
The drift gate fired on this page's artifact twice in an hour, and tracing the
second one found the numbers themselves were wrong in two places. Both are the
shape this artifact exists to stop — a thing the walker could not read, scored
as a thing it had read — wearing opposite hats.
ELEVATION read a spread as a denial. It walked an object literal's named
properties for `isSystem` and returned "not elevated" when the loop ended, so
`{ ...SYSTEM_CTX }` scored decidably NOT elevated while every SYSTEM_CTX in the
tree is `{ isSystem: true }`. It never unwrapped `as const` either, so a bare
`context: SYSTEM_CTX` read as undecidable. 51 verdicts move: 45 undecidable ->
elevated, 6 inverted outright. Decidably-not-elevated is now 0.
TENANT CONTEXT counted sites it had not read. An unreadable options argument was
folded in with one read and found empty, publishing "84 sites carrying no tenant
context at all" when 17 said so and 67 were unread — an over-claim in the
alarming direction, on the figure the page tells other cards to cite. `carries`
is now three-valued; the provable count is 17 (9 tenancy-enabled), with 67 (32)
unreadable.
The generator gains its own `--self-test` (18 cases, 6 red against the old
reading) and CI runs it: these classifiers are published FIGURES, not findings,
so the gate's verdict never covered them and a clean tree could not tell a
working classifier from a weakened one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
`check-step-collectors` caught it: two independent self-tests as a bare sequence in one `run:` block means the second never runs under `bash -e` once the first fails — neither green nor red (#10814). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Regenerated, and the drift trace found two real defects in the census's own classifiersMerged
|
| previous page | now | why | |
|---|---|---|---|
| write call sites | 214 | 215 | a forwarding shim added in backfill-file-references.ts |
| sources scanned | 534 | 534 | — |
| statically decidable / undecidable | 143 / 71 | 143 / 72 | the new site's object name is a parameter |
| carrying no tenant context | 83 (41 tenancy-enabled) | 17 provable (9) + 67 unreadable (32) | defect 2 |
| elevated / not / undecidable | 46 / 8 / 77 | 97 / 0 / 101 | defect 1 |
Cite 9 / 215, and says what it is: sites whose options argument was READ and holds no tenant context, against a decidably tenancy-enabled object — the control's provable yield surface — with an explicit ⛔ against citing it as "the sites without tenant context", because 32 more are neither in nor out. The deviation table is updated; every deviation stays recorded, and the 135 / 77% figure stays un-reproduced (now 97 of 215, 45% decidably elevated, 101 undecidable).
The page now carries a section recording both over-claims and what they cost, rather than quietly correcting numbers it had already published.
New instruments
The generator now has its own --self-test (18 cases), because its classifiers are published FIGURES, not findings: the gate's verdict is "no unplaceable receiver", and the elevation and context verdicts were never part of it — nothing read them, so a clean tree could not tell a working classifier from a weakened one. Ablation, mutation confirmed on disk (blob a1537aa8 → 45c6c135), restore proven by blob equality and empty git diff HEAD: 6 of 18 cases red against the old reading, 13/13 → 18/18 green after.
Also caught and fixed, by re-deriving the family after the merge: check-step-collectors flagged that my two independent self-tests ran as a bare sequence in one run: block — under bash -e the second never runs once the first fails. Now routed through the run_self_test collector.
62 of 64 green on 924d6ea41, exit codes captured before any pipe. The two non-zero are the standing NOT-MEASURED pair (check-test-completeness exit 3; check:type-check-debt needs a full workspace build).
⭐ The judgement you asked for
My read: (c) — and the fix is already in the sibling gate. Not (a), and ⛔ not (b).
What the drift actually is, measured
Over the last 60 non-merge commits on origin/main:
| commits that… | share |
|---|---|
touch the corpus at all (can move sources scanned) |
25% |
| touch a file holding a write call site | 15% |
| actually move a write call or its context line | 7% |
| touch the corpus but no site-bearing file — pure scale churn | 10% |
⇒ 40% of all drift-capable commits (10 of 25) carry no possible population change. They can only move sources scanned. That is exactly what drift #1 was: a required gate went red over a number with no security content.
The artefact is carrying two things with different volatilities, and the gate holds both to one standard
- the population — 215 sites, the per-file rows, the tenancy and context postures. Security-relevant. Moves on 7% of commits.
- the scale figures —
sources scanned, engine-shaped types, declared objects, non-engine subtracted. Provenance. Move on 25%.
⭐ The sibling already solved this, for this exact reason. check-system-context-census.mjs splits CENSUS-DERIVED numbers (enforced) from WHOLE-CORPUS TEXT counts (present and dated, ⛔ not enforced), and its own docblock gives the reason in as many words: those are "numbers about a population the page does not certify — and whose churn, measured, was blocking the page from ever landing."
I inherited the sibling's anchor-scheme argument and not this part of its design. Drift #1 was the bill. Had sources scanned been unenforced, drift #1 would not have happened at all; drift #2 would still have gone red, correctly.
Why ⛔ not (b), the recorded baseline
It is the intuitive fix and I think it is the wrong one. Pinning the gate to a recorded sha means the page is checked against a commit nobody re-checks, so it can no longer answer "a write call site arrived and nothing documented it". That is the census → page direction, and it is the whole reason this artefact is worth more than the comment it replaced — the sibling's docblock is blunt about it: "a gate that only checks what the page already says can never find what the page failed to say."
⇒ (b) does not remove the staleness, it removes the alarm. It re-creates the failure this card was filed for, in slow motion and with a green check on top.
What I would put to the maintainer
- Adopt the sibling's enforced/unenforced split here. Measured: removes 40% of drift-capable commits, and removes the only class that is pure noise. Cheap, precedented, and it does not weaken a single security-relevant assertion.
- Know that it is necessary but may not be sufficient for THIS PR. ~7% of commits still move the population — roughly one a day at this tree's rate. A governed PR needing a human merge has a window longer than that, and
⚠️ you are right that the merge queue is no escape: it rebuilds and re-runs but does not regenerate, so the last regeneration has to happen close to the merge. That is a scheduling problem, not a design one, and it is the residue the split cannot remove. - A question I cannot answer and would not guess at: whether an artefact this hot should be generated and committed by CI rather than by an author. That changes who owns the number, and it is a governance call.
⛔ I have implemented none of this.
Generated by Claude Code
…nant-audit-census-artifact
`dispatch-gates --self-test` went red on this branch, by name:
✗ promoting 11 module(s) to gate files subtracts no inherited hint from any
other family — LOST: scripts/check-tenant-audit-census.mjs <-
scripts/tenant-audit-census.mjs (packages/services, packages/plugins, …)
Wiring `node scripts/tenant-audit-census.mjs --self-test` into lint.yml made the
generator a GATE FILE, and `discoverFamilies` refuses to follow one. The gate
spelled ZERO path literals of its own and inherited all 22 from that module, so
the promotion cut every one — a PR touching `packages/services/**`, the exact
population this census counts, would silently stop being told this gate reads
its diff. A lead that stops appearing is indistinguishable from one never earned.
Fixed at the wiring rather than by weakening the assertion. The generator drops
its `--self-test` FLAG and keeps its `selfTest()`, which the gate's own
`--self-test` now drives — so the 18 classifier cases still run on every CI pass
while the module stays followable. lint.yml goes back to two commands, so the
collector this step briefly needed is no longer one.
Belt and braces: the gate now carries a `ROOT_DIR_WATCH_HINTS` provenance
declaration naming the two corpus subtrees and both artefacts, so the next
attempt to invoke the generator directly cannot silently cut discoverability
again. Family hints: 0 -> 24.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Root-caused. It is (a) — my own
|
| hints spelled | |
|---|---|
scripts/check-tenant-audit-census.mjs |
0 |
scripts/tenant-audit-census.mjs |
22 |
The gate had no literals of its own and inherited all 22. Adding node scripts/tenant-audit-census.mjs --self-test to lint.yml promoted the generator to a gate file — and cut every one.
⇒ A PR touching packages/services/** — the exact population this census exists to count — would have stopped being told this gate reads its diff. And per the assertion's own docblock: "a lead that stops appearing is indistinguishable from a lead that was never earned, so nothing in the output would say so." Silent coverage loss on the census's own corpus. The assertion earned its keep.
Fixed at the wiring, ⛔ not by weakening anything
The real fix is to stop promoting the module:
- the generator drops its
--self-testflag and keeps itsselfTest(); - the gate's
--self-testdrives it, so all 18 classifier cases still run on every CI pass; lint.ymlgoes back to two commands — which also means therun_self_testcollector that step briefly needed is no longer needed, and it is gone.
⛔ I did not add a row to SELF_TEST_RUN_OTHERWISE; that ledger "only ever shrinks" and widening it is not mine to do.
Belt and braces: the gate now carries a ROOT_DIR_WATCH_HINTS provenance declaration — the rostered idiom, same as check-role-word.mjs — naming the two corpus subtrees and both artefacts. It is not what fixes the assertion; it is what stops the next attempt to wire the generator directly from silently cutting discoverability again.
Verified, not assumed — the family's hints went 0 -> 24, and coverage is real:
family: scripts/check-tenant-audit-census.mjs selfTest: false hints: 24
covers packages/services/service-storage/src/metadata-store.ts -> true
covers packages/plugins/plugin-security/src/bootstrap-platform-admin.ts -> true
covers content/docs/permissions/tenant-audit-census.mdx -> true
Results on the merged tree, fc8858a24
✓ dispatch-gates self-test: 1017 cases pass.
✓ check-watch-hint-literal: 34 declaration(s) across 4 rostered name(s)
✓ tenant-audit-census self-test: 18 cases pass
✓ check-tenant-audit-census self-test: 10 cases pass
✓ check-tenant-audit-census: OK -- 215 write call sites certified (143 decidable;
9 tenancy-enabled sites PROVABLY carry no tenant context, 32 more unreadable),
24 prose figures held to the census
64 green, exit codes captured before any pipe. The two non-zero are the standing NOT-MEASURED pair (check-test-completeness exit 3, check:type-check-debt needs a full workspace build). The census did not drift this round — both artefacts came back already current against the merged tree, so no figure moved.
One page edit: the prose named tenant-audit-census.mjs --self-test as the instrument, and that flag no longer exists — it now names check-tenant-audit-census.mjs --self-test, which is where those 18 cases actually run.
⛔ Unchanged: both classifier fixes, three-valued carries, Cite 9 / 215, every recorded deviation, 135/77% un-reproduced. ⛔ Still draft, ⛔ not armed. ⛔ Nothing from #13627 implemented here.
Generated by Claude Code
…orced corpus scale Maintainer ruling 2026-08-31 (option A): the census gate adopts the enforced/unenforced split the sibling `check-system-context-census.mjs` already proved. Population -- every write call site and its tenancy/context verdict -- stays enforced byte-for-byte. Corpus scale (sources scanned and its kin) becomes present, dated, and not compared. The sibling's mechanism does NOT transplant: it splits a list of regexes over a hand-written page, while this gate's enforced half is a byte comparison of generated text with no per-number comparison to drop. So the scale values are masked out of both sides of that comparison, and required to be present and dated instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Heads-up:
|
⏸ Standing status: green for 10 hours, and the only thing left is a human merge
Why no AI seat has landed it. The diff touches
⛔ This seat is still not flipping it ready, and the reason is case law rather than caution: ⇒ Waiting on the maintainer. The question of whether green governed PRs should be marked ready Generated by Claude Code |
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33404459759 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #13548
Fixes #13627
Implements the maintainer's ruling (2026-08-31, director batch #10, verbatim「同意」): option A — the tenant-audit census re-derived as an in-tree artifact on the
isSystemcensus pattern (page + generator + gate). Delivered standalone, per the dispatching seat's delegated call; #13497 is untouched.What the artifact is
scripts/tenant-audit-census.mjs--json,--writecontent/docs/permissions/tenant-audit-census.mdxdocs/audits/2026-08-tenant-audit-write-call-sites.counts.mdscripts/check-tenant-audit-census.mjs--self-test.github/workflows/lint.ymlisSystemsibling, in a job with no paths filterThe process footnote the ruling asked to ride on A is on the page (
## ⚠️ Why this page exists at all): the artifact exists because #13178's issue carrier vanished and three open cards' dependency on it went unnoticed. No separate card.⭐ The measurement, and the deviations — untrimmed
isSystemguardNothing was shaped to match. The old population definition is unrecoverable — that is the whole reason this artifact exists — so the page states what this instrument counts and does not attribute the gap. Two structural facts plausibly widen this reading against any hand or regex one, and both are counted on the page: 45 sites reached through an
any-typed receiver, and 37 that name their object through aconstrather than inline.9 / 215, both halves re-derivable.How the population is decided
A site is a call to one of the three
IDataEnginewrite doors (insert/update/delete) on a receiver whose declared type is an engine, in tracked non-test sources underpackages/services/andpackages/plugins/.The verb name is not the signal —
.delete()alone answers ~250 sites here, mostlyMap/Set/Headers/crypto-Hash/blob-storage/search-index. So receivers are typed structurally: a declared interface or type literal is an engine when it declares a write door whose first parameter is namedobject/objectName/objectApiName/nameand typedstring. Interfaces thatextendone inherit it; aliases that narrow one (Partial<Pick<IDataEngine, …>>) carry it. 56 engine-shaped types found, where a name list would have found the handful someone remembered.anyis not a classification. 45 sites — better than a fifth of the population, concentrated in exactly the seed and bootstrap paths this control exists for — have receivers the author erased. Scoring those "not an engine" drops them silently with a clean exit. They are placed by facts about the tree (the argument names a declared object; the argument is a parameter declaredobject: string) or by a writtenUNTYPED_RECEIVERSrow. An unplaceable receiver is an error, never a default — production run reportsunresolved receivers 0.Neither artifact carries line numbers. Rows aggregate by (file, verb, object, tenancy, context posture), which is invariant under displacement, so the only thing that can move them is the population. That skips #13490's whole defect class on day one: anchors that rot on a pure displacement, plus a
--fixarm that then has to tell displacement from a population change and gets it wrong. There is exactly one repair path here (--write), it is mechanical, and it has no case where it must guess.scripts/check-system-context-census.mjsandscripts/isystem-census.mjswere read-only throughout; neither is touched.Evidence
Gate ablation — commit first, mutate, confirm on disk, restore, prove the restore. A new engine write call site injected into
packages/services/service-datasource/src/datasource-secret-binder.ts:--self-test: 10 cases pass (at first delivery; 19 after the split below) — drift on a dropped ledger row, on an edited page count, on an edited ledger total, on a lost marker region; prose drift; a prose claim reworded out of the gate's reach (which must red, or every prose rule retires by deleting a sentence); and an empty census refusing rather than certifying.Gates run locally on final
903150746, all green — the derived family fromscripts/pm/dispatch-gates.mjs(37 runs), pluscheck-self-test-wired,check:merge-driver,check:nul-bytes, and the new gate. Each exit code captured before any pipe.check:role-wordandcheck:docs-audit-scopeboth red first and were repaired at the source, not baselined.check:type-check-debtis NOT MEASURED:--re-measurerefuses without a full workspace build (56 dependencies unbuilt), and it is derived here only because this PR editslint.yml. Nothing in this diff can move package type debt.check:type-check-coverage, the same script without--re-measure, is green.Notes for the PM
content/docs/**,docs/audits/**,scripts/**,.github/workflows/**,.claude/**only.skip-changesetapplied.docs/audits/…counts.mdis regenerated whole and so carries the exact merge pathologymerge=os-regenexists for (two branches each adding a site → rows merge cleanly, totals merge cleanly and wrong). It could not be registered:scripts/git-merge-regen.mjsresolves an artifact'sgen:/check:names inpackages/spec/package.jsononly, and these are root-level tooling. Generalizing that resolver is out of scope for this card. The gate is the backstop — a wrongly merged file fails loudly — and the ledger says so in its own header.⭐ Update — the enforced / unenforced split (ruling 2026-08-31, option A)
Implements the maintainer's ruling on #13627 (director batch #12, verbatim「同意」). #13627 is a decision card and remains open — the PM triages it.
The precedent the ruling rests on
scripts/check-system-context-census.mjs:77-84, quoted because the ruling rests on it:And the reason option B (assert against a recorded baseline sha) stays closed, from
:30of the same file:What changed
scripts/tenant-audit-census.mjscorpusScaleRows,renderCorpusScale,measuredAt)scripts/check-tenant-audit-census.mjsdeclared objects in the registrymoves fromPROSE_COUNTStoUNENFORCED_PROSE_COUNTS.github/workflows/lint.yml534de-quoted — same defect class, one lineThe line follows one the data already draws: everything in
census.totalsis the population the page certifies and stays enforced byte-for-byte; the four scalars beside it describe the corpus walked. ⛔ Nothing security-relevant moved: every write call site, every row, every tenancy and context verdict, the empty-census refusal and every enforced prose figure are untouched.The dispatch assumed (A2.1) the sibling's split is a pattern to follow. It is not, and the difference is structural. The sibling enforces a hand-written page through a list of regexes, so it splits by simply not comparing some of them. This gate's enforced half is a byte-for-byte comparison of generated text — there is no per-number comparison to drop. Adding an unenforced list beside the byte compare would have changed nothing: the byte compare still reds on the scale line.
⇒ The outcome the ruling names transplants; the mechanism does not. The values are masked out of both texts instead, and everything else stays byte-exact. That is recorded in
maskUnenforced.The card states 25% of commits move
sources scanned. Re-measured over the 60 most recent non-merge commits reachable from967402a12:⇒ The classifier behind the 25% counted the touch, not the move. The direction of the card's argument holds; the magnitude of the benefit is roughly a seventh of what it claimed.
⭐ The measurement that draws the line where it is drawn
The census was run at every one of the 16 corpus-touching commits (the other 44 cannot move either artefact), and the rendered artefacts diffed line by line — not the totals, since the byte comparison is over the whole text:
sources scanned)⭐ The control points both ways. On the two scale-moving commits every population figure was byte-identical; on the one population-moving commit every corpus-scale number was byte-identical. The two classes did not move together once.
Evidence — before and after, on the real gate
Both runs use the committed artefacts and a real tree carrying one added non-test source with no write call site — the pure-noise class, 2 of the 3 measured drifts:
⛔ And the control, without which the above only proves the gate went blind — a tree carrying an added source that does carry write call sites, scale drifted to 536 at the same time:
The scale count moved by 2 and was ignored; the population moved by 1 and was caught.
Gate self-test
node scripts/check-tenant-audit-census.mjs --self-test— 19 cases (was 10), the new ones driving the split in both directions:Across N declared objects, which is now unenforced; they were re-pointed at an enforced prose figure rather than deleted. One of those rewrites first replaced nothing — the page hard-wraps at 80 columns and the sentence spans two lines — which the self-test caught as a red. The anchor is now the bold run alone, and the trap is written down beside it.Remaining reds after the split — the data the ruling's option-C paragraph asked for
The split is necessary and not sufficient, and the honest residual is smaller than assumed. PM assumption A2.2 predicted roughly 7% per day; measured, population drift is 1 in 59 commits (1.7%). That still lands inside a governed-surface human-merge window, so the scheduling question option C was parked on is real but rarer than estimated. ⛔ No option-C work is done here.
A2.3 held: nothing else reads the scale counts as enforced.
.claude/workflows/docs-accuracy-audit.jslists the page in a docs inventory only; the sole enforcement points were check A and the onePROSE_COUNTSrow, both handled.Gates
Repo-wide
pnpm lintpassed at head82025f57c(exit 0, 99s, throughscripts/pm/os-verify-lock.sh), plus the derived family fromnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 42 runs, all green, exit codes captured before any pipe. Includes both ledger obligations the tool names for a gate-script edit:pnpm check:pm-dispatch-gatesandnode scripts/pm/bare-root-worklist.mjs --self-test.check:nul-bytesgreen over 7546 files.check:type-check-debtremains NOT MEASURED for the same reason as before — its--re-measureleg refuses without a built workspace closure, and this diff adds no TypeScript. Its self-test andcheck:type-check-coverageare green.⛔ Governed surface — stays a draft. This PR touches
.claude/workflows/docs-accuracy-audit.js, so it is judged on the file list: not marked ready, no auto-merge, no queue. A human merges it.Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Generated by Claude Code
Generated by Claude Code