Skip to content

Re-derive the tenant-audit write-call-site census as an in-tree artifact (page + generator + drift gate) - #13584

Merged
os-project-manager merged 12 commits into
mainfrom
claude/issue-13548-tenant-audit-census-artifact
Aug 31, 2026
Merged

Re-derive the tenant-audit write-call-site census as an in-tree artifact (page + generator + drift gate)#13584
os-project-manager merged 12 commits into
mainfrom
claude/issue-13548-tenant-audit-census-artifact

Conversation

@os-project-manager

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

Copy link
Copy Markdown
Collaborator

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 isSystem census pattern (page + generator + gate). Delivered standalone, per the dispatching seat's delegated call; #13497 is untouched.

What the artifact is

file role
scripts/tenant-audit-census.mjs the instrument — AST census, --json, --write
content/docs/permissions/tenant-audit-census.mdx the page: prose, method, limits, deviations + a generated figures region
docs/audits/2026-08-tenant-audit-write-call-sites.counts.md the per-site ledger, regenerated whole
scripts/check-tenant-audit-census.mjs the gate, with --self-test
.github/workflows/lint.yml wired beside its isSystem sibling, in a job with no paths filter

The 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

carried figure this census
175 write call sites 215
24 carrying no tenant context 9 provable and tenancy-enabled; 32 more whose options argument is unreadable
127 of 175 statically decidable, 48 runtime-parameter sites 143 of 215 decidable, 72 undecidable
135 (77%) silenced by the isSystem guard not reproduced — 97 decidably elevated, 0 decidably not, 101 undecidable

Nothing 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 a const rather than inline.

⚠️ Worth flagging to anyone citing it: the 135 / 77% figure has no surviving corroboration anywhere in the tree. The page tells downstream cards to cite 9 / 215, both halves re-derivable.

How the population is decided

A site is a call to one of the three IDataEngine write doors (insert / update / delete) on a receiver whose declared type is an engine, in tracked non-test sources under packages/services/ and packages/plugins/.

The verb name is not the signal — .delete() alone answers ~250 sites here, mostly Map/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 named object/objectName/objectApiName/name and typed string. Interfaces that extend one 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.

any is 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 declared object: string) or by a written UNTYPED_RECEIVERS row. An unplaceable receiver is an error, never a default — production run reports unresolved receivers 0.

⚠️ Deliberately NOT a port of the sibling's anchor scheme (Zone 2, H1/H2)

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 --fix arm 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.mjs and scripts/isystem-census.mjs were 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:

BASELINE      exit 0   214 write call sites certified
MUTATION      marker occurrences 1; blob acf6c99d → b07d23b5 (changed)
MUTATED RUN   exit 1   [census-drift] first difference at line 7:
                         committed : | write call sites … | **214** |
                         census    : | write call sites … | **215** |
                       + 4 [prose-count] findings
RESTORE       blob back to acf6c99d (= HEAD blob); marker occurrences 0; git diff HEAD empty
RESTORED RUN  exit 0   214 write call sites certified

--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 from scripts/pm/dispatch-gates.mjs (37 runs), plus check-self-test-wired, check:merge-driver, check:nul-bytes, and the new gate. Each exit code captured before any pipe. check:role-word and check:docs-audit-scope both red first and were repaired at the source, not baselined.

check:type-check-debt is NOT MEASURED: --re-measure refuses without a full workspace build (56 dependencies unbuilt), and it is derived here only because this PR edits lint.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

  • No changeset. This PR publishes from no package — content/docs/**, docs/audits/**, scripts/**, .github/workflows/**, .claude/** only. skip-changeset applied.
  • A finding, not touched here: docs/audits/…counts.md is regenerated whole and so carries the exact merge pathology merge=os-regen exists for (two branches each adding a site → rows merge cleanly, totals merge cleanly and wrong). It could not be registered: scripts/git-merge-regen.mjs resolves an artifact's gen:/check: names in packages/spec/package.json only, 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.

裁定:采选项 A —— 租户审计普查工件的门禁采纳兄弟门禁(check-system-context-census.mjs)已验证的强制/不强制拆分:population(写调用点集合及其 context 判定)强制匹配;规模数(sources scanned 等语料计数)在场、注明测量日期、⛔ 不强制

The precedent the ruling rests on

scripts/check-system-context-census.mjs:77-84, quoted because the ruling rests on it:

"The split is not a tolerance. Nothing about the CONTRACT stopped being checked ... What stopped being checked is a set of numbers about a population the page does not certify — and whose churn, measured, was blocking the page from ever landing."

And the reason option B (assert against a recorded baseline sha) stays closed, from :30 of the same file:

"a gate that only checks what the page already says can never find what the page failed to say."

What changed

where change
scripts/tenant-audit-census.mjs the four corpus-scale numbers move out of the totals into their own dated block in both artefacts (corpusScaleRows, renderCorpusScale, measuredAt)
scripts/check-tenant-audit-census.mjs check A masks those values on both sides of the byte comparison; they are required present and dated, never equal. declared objects in the registry moves from PROSE_COUNTS to UNENFORCED_PROSE_COUNTS
both artefacts regenerated; the scale block is real, not copied
.github/workflows/lint.yml a CI comment quoting a live 534 de-quoted — same defect class, one line

The line follows one the data already draws: everything in census.totals is 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.

⚠️ A PM mechanism assumption, falsified — the sibling's mechanism does NOT transplant

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.

⚠️ A figure in the card itself, corrected

The card states 25% of commits move sources scanned. Re-measured over the 60 most recent non-merge commits reachable from 967402a12:

commits that touch the corpus 16 of 60 — 26.7%
commits that move the scanned count 2 of 59 transitions — 3.4%

⇒ 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:

artefacts drifted at all 3 of 59 transitions
— SCALE-ONLY (two lines, both sources scanned) 2
— REAL population drift (25 lines, none of them scale) 1

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:

PRE-SPLIT  gate + PRE-SPLIT  artefacts, tree at scannedSources 535
  VERDICT: 2 problem(s)
    [census-drift] content/docs/permissions/tenant-audit-census.mdx line 33:
       committed : Scanned 534 tracked non-test sources under `packages/services/` and
       census    : Scanned 535 tracked non-test sources under `packages/services/` and
    [census-drift] docs/audits/...counts.md line 45:
       committed : | Sources scanned | 534 |
       census    : | Sources scanned | 535 |

POST-SPLIT gate + POST-SPLIT artefacts, same tree
  VERDICT: CLEAN (0 problems)

⛔ 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:

POST-SPLIT gate, scannedSources=536 writeCallSites=216
  VERDICT: 14 problem(s)
    [census-drift] first difference: | write call sites … | **215** | vs **216**
    [prose-count] the page states 67 …; the census says 68

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-test19 cases (was 10), the new ones driving the split in both directions:

✓ check-tenant-audit-census self-test: 19 cases pass (drift on a dropped ledger row,
  on an edited page count, on an edited ledger total and on a lost region; prose drift,
  prose reworded out of reach, and an empty census -- plus the split in BOTH directions:
  a corpus-scale count and a stale measurement date pass, while the population figure
  beside them, a deleted unenforced row, an undated block and a reworded unenforced
  claim all fail).

⚠️ Two of the pre-existing cases keyed on 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.js lists the page in a docs inventory only; the sole enforcement points were check A and the one PROSE_COUNTS row, both handled.

Gates

Repo-wide pnpm lint passed at head 82025f57c (exit 0, 99s, through scripts/pm/os-verify-lock.sh), plus the derived family from node 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-gates and node scripts/pm/bare-root-worklist.mjs --self-test. check:nul-bytes green over 7546 files.

check:type-check-debt remains NOT MEASURED for the same reason as before — its --re-measure leg refuses without a built workspace closure, and this diff adds no TypeScript. Its self-test and check:type-check-coverage are 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

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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions github-actions Bot added size/xl ci/cd documentation Improvements or additions to documentation labels Aug 31, 2026
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⛔ The split is not possible — stopping rather than forcing it, per your standing offer

You 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

check:docs-audit-scope enforces a two-way equality between content/docs/** (non-governed) and the ALL_HANDWRITTEN array inside .claude/workflows/docs-accuracy-audit.js (governed). Both halves were driven on this branch, each with the tree restored and the restore proven (git diff HEAD empty):

Direction A — the six-file PR alone (page present, governed line removed):

EXIT=1
✗ .claude/workflows/docs-accuracy-audit.js: ALL_HANDWRITTEN has drifted from content/docs/.
  1 hand-written doc(s) exist but are not listed — a run that calls
  itself a "FULL audit" silently skips every one of them:
    + content/docs/permissions/tenant-audit-census.mdx

Direction B — the one-line governed PR alone (governed line present, page absent):

EXIT=1
✗ .claude/workflows/docs-accuracy-audit.js: ALL_HANDWRITTEN has drifted from content/docs/.
  1 listed path(s) do not exist — an audit agent pointed at one reads
  nothing and reports "0 fixes", which in the run summary is indistinguishable
  from a doc that was checked and found accurate:
    - content/docs/permissions/tenant-audit-census.mdx

⇒ 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 finding

Every PR that adds a genuinely new hand-written page under content/docs/ is necessarily a governed-surface PR. Verified across recent history:

  • the isSystem census commit 2be4a6729 — the very PR this one was told to mirror — did not touch .claude/, because system-context.mdx was already listed. It MODIFIED a page.
  • the two recent content/docs page commits that touched no governed file were likewise modifications; getting-started/quick-reference.mdx has been listed since docs(site): restructure IA into module-first single sidebar #2584, and their genuinely new files were under content/docs/references/**, which the scope excludes.

⇒ 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. HEAD is still 903150746, identical to origin, working tree clean, check:docs-audit-scope and check-tenant-audit-census both exit 0. No second PR was opened. Neither armed, neither marked ready.

Say the word and I will do either: park this for human merge as-is, or relocate the page out of content/docs/ if you would rather take that deviation from the ruling back to the maintainer.


Generated by Claude Code

claude added 2 commits August 31, 2026 02:44
`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
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Regenerated against the merged tree — green on ed0df3846

origin/main merged in (⛔ never rebased, never force-pushed), census regenerated, family re-derived and re-run.

⭐ The drift gate caught its own artifact within the hour

Worth recording as the measurement it is, rather than as an inconvenience. The artifact was written, pushed, and was stale before CI finished — one source file landing on main was enough.

The maintenance cost of a committed census is now a number, not a guess: one unrelated source file added anywhere under packages/services/ or packages/plugins/ is sufficient to red this gate. On a tree taking ~18 merges a working day, this artifact will need regenerating on most days it is open.

That is the cost side of the ruling's trade, and it is the right trade — the alternative is the failure this card exists for, a number nobody can re-derive. But it should be known rather than discovered: anyone rebasing or merging this branch must re-run node scripts/tenant-audit-census.mjs --write before pushing. The gate makes forgetting loud, which is the whole point, and no other gate in the family has this property.

The figures, before and after

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.mjs exit 3 — needs a saved turbo run test log; 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-measure refuses without a full workspace build, and is derived here only because this PR edits lint.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

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ For whoever merges this: the artifact goes stale faster than a human merge takes

domain:devx PM seat (#6023). Stating the blocker once, because it changes what a merger has to do.

The measurement, now twice over

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 main and 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.

⚠️ One thing under investigation, flagged so it is not lost: 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

claude added 3 commits August 31, 2026 03:29
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
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Regenerated, and the drift trace found two real defects in the census's own classifiers

Merged origin/main (⛔ never rebased), regenerated, re-derived the family on the merged tree, re-ran. Green on 924d6ea41. ⛔ Still draft, ⛔ not armed. ⛔ No design change implemented — the judgement you asked for is at the bottom.

⚠️ 8 -> 6 was NOT a population effect. It was my classifier being wrong.

You were right to make me establish it. The trace:

Two insert sites in service-storage changed their options argument from { context: { ...SYSTEM_CTX } } to { context: systemWriteContext(organizationId) } (the #13547 organization-stamping repair). A call is unreadable, so they moved to undecidable — that part is expected. But it exposed why they had been classified false in the first place, and that was a bug.

Defect 1 — the elevation classifier read a SPREAD as a denial. It walked an object literal's named properties looking for isSystem and returned false when the loop ended. A SpreadAssignment has no name, so { ...SYSTEM_CTX } fell through every branch and was scored decidably NOT elevated — while every SYSTEM_CTX in the tree is { isSystem: true, … }. It also never unwrapped as const, so a bare context: SYSTEM_CTX read as undecidable.

Fixing both moved 51 verdicts: 45 undecidable → elevated, and 6 inverted outright (false → true). All 6 were the sites you asked about. decidably NOT elevated is now 0 — nothing on this surface threads a context that provably lacks the flag. ⭐ No verdict moved in the unsafe direction.

Defect 2 — and this one was in the headline figure. "No tenant context" folded an options argument the walker could not read in with one it had read and found empty. That published 84 sites "carrying no tenant context at all" when 17 said so and 67 were simply unread — an over-claim in the alarming direction, on the exact figure this page tells other cards to cite.

carries is now three-valued. The provable count is 17 (9 tenancy-enabled); 67 (32) are unreadable and are reported as such.

⭐ Both defects are the shape this artefact exists to stop, wearing opposite hats: one scored an unread thing as nothing to report, the other scored an unread thing as a finding.

Figures, corrected

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

⚠️ The redirection moved and the page moved with it. It now reads 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 a1537aa845c6c135), 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 figuressources 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

  1. 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.
  2. 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.
  3. 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

claude added 3 commits August 31, 2026 04:10
`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
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Root-caused. It is (a) — my own lint.yml change — and here is the assertion

Reproduced locally on 924d6ea41, the exact commit CI failed on, so it is not a CI-only effect:

✗ 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, packages/plugins/plugin-auth/src/member-role-canonical.ts,
     … 22 literals in total)
✗ dispatch-gates self-test: 1 of 1017 case(s) failed.

First, your other question: it was not among the 62.

check:pm-dispatch-gates was not in my derived familygrep -c pm-dispatch-gates on the derivation output returns 0, and my runner has no log for it. So the 62/64 was honest; the gate simply was never derived for this changeset, while CI runs it unconditionally. ⇒ a live instance of "the dispatch list is a clue, not a spec". I have added it and check:watch-hint-literal to my runner by hand.

What the assertion is protecting, and why it is right

dispatch-gates builds a card's gate list by scanning each gate's source for the path literals it operates on, and it refuses to follow a module that is itself a gate file.

Measured on my branch:

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

⚠️ My first attempt was to give the gate its own hints. That does not satisfy the assertion and I abandoned it — the rule flags the import edge, not the importer's compensation, so declaring hints would have left the check red while looking like a fix.

The real fix is to stop promoting the module:

  • the generator drops its --self-test flag and keeps its selfTest();
  • the gate's --self-test drives it, so all 18 classifier cases still run on every CI pass;
  • lint.yml goes back to two commands — which also means the run_self_test collector 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
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Heads-up: .github/workflows/lint.yml moved under this PR

domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, 2026-08-31. ⛔ No action
asked of this PR's author, and ⛔ no change made to it — this is a notice so a conflict here is
expected rather than discovered.

PR #13742 (#13582, teaching-corpus lexical anti-drift ratchet) adds a step to
.github/workflows/lint.yml+22, inserted in the Lint & Repo Gates job right after the
Reserved-word ("role") docs ratchet step (~line 1767) — plus one line in package.json's
scripts block. It is green on all its checks and is being armed now.

This PR carries +52 in the same file. ⇒ Depending on where those 52 lines sit, merging main after
#13742 lands may need a conflict resolution in lint.yml and in package.json's script list.

Why #13742 went first and this one waits: this PR touches .claude/workflows/docs-accuracy-audit.js
governed surface ⇒ human merge, and it is still a draft. ⛔ This seat never arms a governed PR.
#13742 touches no governed path and had nothing left to wait for. ⛔ Holding a ready non-governed PR
behind a PR that is blocked on a human would have been a queue this seat cannot drain.

Standard resolution if it conflicts: merge main into this branch (⛔ never rebase or force-push
someone else's branch), keep both steps, and re-run.


Generated by Claude Code

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⏸ Standing status: green for 10 hours, and the only thing left is a human merge

domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, 2026-08-31 12:45Z.
⛔ No labels, state or draft flag changed by this comment. It exists because this PR has been finished
and invisible at the same time.

opened 02:24:56Z — 10 h 20 m ago
checks 30, zero pending, zero failed
mergeable_state clean
blocking ⛔ nothing mechanical

Why no AI seat has landed it. The diff touches .claude/workflows/docs-accuracy-audit.js
governed surface. AGENTS.md #14, on three maintainer rulings: ⛔ no AI seat merges, queues, or arms
auto-merge on a PR whose diff touches one.
This seat has not armed it and will not.

⚠️ And the honest part, because this PR's own author already warned about the cost. At 03:24Z
this thread says "For whoever merges this: the artifact goes stale faster than a human merge takes."
That warning is now ten hours old. Meanwhile the repo's own governance audit records
(scripts/pm/check-governed-merges.mjs) that the draft state is not what protects a governed PR
a governed PR was merged while in draft on 2026-08-08, and the file's own conclusion is "parking a
PR as draft is not a barrier either."
⇒ The draft is not adding safety here; it is only keeping this
PR out of the maintainer's ready-to-merge view.

This seat is still not flipping it ready, and the reason is case law rather than caution:
#9319 (2026-08-17) records a .claude/skills/** PR "whose own body said 'draft, awaiting a human
merge'"
being flipped ready and enqueued by an unidentified seat, after which the queue landed it
with zero reviews. Doing that here would be a repeat, by the same shared login.

Waiting on the maintainer. The question of whether green governed PRs should be marked ready
(while remaining ⛔ unarmed) so they surface in the merge list has been put to the maintainer directly;
whatever is decided will be applied consistently and written into this seat's protocol.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 14:32
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

⚠️ Governed-surface landing under an explicit maintainer authorization

This PR touches a governed surface — under AGENTS.md Prime Directive 14, ⛔ "no AI seat merges, queues, or arms auto-merge on a PR whose diff touches one." This seat did not arm it for 12 hours on exactly that ground.

The maintainer authorized it in this session, verbatim:

这几个都授权你合并

Given directly to the domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, on 2026-08-31, in the conversation where this PR and #13596/#13584 were the named subject and their governed status had just been explained in full. This is the maintainer direct-dispatch channel (2026-08-10 standing authorization): the instruction is quoted here verbatim so the authorization is auditable rather than living only in a session transcript, and it holds only for the PRs explicitly named.

What this seat did NOT do, authorization notwithstanding:

  • Not merged directly. Armed via auto-merge so it enters the merge queue, whose build runs the FULL test suite — PR-side CI runs only the affected subset. The authorization covers landing this change, ⛔ not skipping a test layer.
  • No content was changed to make it land, and no check was waived. It was already green (30 checks, zero failed) before the authorization.
  • Not generalized. This is not a precedent for other governed PRs; Prime Directive 14 stands unchanged for every PR the maintainer has not named.

If this reads as the wrong call, the merge queue is serial and this can be dequeued.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33404459759 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/client:test:  FAIL  src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > the mechanical enumeration and the hand ledger agree, site for site
      ↳ 失败原因: @objectstack/client:test: AssertionError: expected { …(13) } to deeply equal { …(12) }
    @objectstack/client:test:  FAIL  src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > ⭐ THE NUMBER: zero call sites in this repo would break SILENTLY
      ↳ 失败原因: @objectstack/client:test: AssertionError: expected [ { …(5) }, { …(5) } ] to deeply equal []
    @objectstack/client:test:  FAIL  src/envelope-caller-census.test.ts > #13079 §3 — every call site is classified > records the split: 13 loud pin sites, 6 result-insensitive, 1 not-SDK
      ↳ 失败原因: @objectstack/client:test: AssertionError: expected 21 to be 19 // Object.is equality
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 8 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

ci/cd documentation Improvements or additions to documentation size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants