Skip to content

fix(devx): tenant-audit-census self-test fixture built from the page's own number, not the live census - #17661

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-17437-census-selftest-value-free
Sep 11, 2026
Merged

fix(devx): tenant-audit-census self-test fixture built from the page's own number, not the live census#17661
baozhoutao merged 1 commit into
mainfrom
claude/issue-17437-census-selftest-value-free

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes #17437

What

scripts/check-tenant-audit-census.mjs's self-test built the fixture for its
two UNENFORCED_PROSE_COUNTS cases (⭐ a stale unenforced prose number is NOT a finding / an unenforced prose claim reworded off the page IS a finding)
from the live census (census.declaredObjects), not from the page's own
stated number. That figure is declared unenforced by the gate's own contract
(:840, "value free, sentence required"), so the moment the page's stated
number and the live tree disagree, the live-built search string stops
matching the page, .replace() becomes a silent no-op, and the second case
fails for the page, not for the classifier it exists to pin.

The fix reads the page's own number with the same UNENFORCED_PROSE_COUNTS[0]
pattern the classifier itself uses, and builds both cases' fixtures from
that instead. The pair now tests the classifier's behaviour regardless of
whether the page and the live census currently agree — which is the gate's
own contract, restored to its self-test.

Why

This is Route 1 from the PM's ruling on #17437: make the self-test
value-free, not the number enforced. The counter over-match issue named in
the card (declaredObjects() counting nested name: literals, not just
top-level object declarations) is out of scope here — it is Route 2's
prerequisite and changes a published figure; see Open Questions below for
whether it should be filed separately.

Verification

  • node scripts/check-tenant-audit-census.mjs — exit 0 (unchanged; PR-verdict gate).
  • node scripts/check-tenant-audit-census.mjs --self-test — exit 0, 19/19 cases
    pass (unchanged case count and names; battery floor holds).
  • Re-measured on origin/main before editing (not the shared checkout — via
    git show origin/main:PATH and a dedicated worktree + fresh pnpm install):
    the page currently states 300 declared objects
    (content/docs/permissions/tenant-audit-census.mdx:87,233) and
    runCensus().declaredObjects is also 300 — they currently agree, so
    --self-test is green on origin/main today. This matches triage's R+168
    correction: the earlier main-red reading came from a working tree that
    wasn't origin/main, and fix(plugin-security): refuse ADR-0068 built-in identity names at both position write doors #17436's --write regeneration cleared the
    symptom for this commit, as the card itself predicted it would.
  • Reverse verification (both legs, in-memory only — the real
    content/docs/permissions/tenant-audit-census.mdx was never edited):
    • Leg 1 — manufacture a divergence. Built a synthetic in-memory page
      where the stated number differs from the live census. The OLD fixture
      logic (built from census.declaredObjects) fails the "reworded off the
      page IS a finding" case exactly as the card describes. The NEW logic
      (built from the page's own number) passes both cases — the self-test
      stays green under the divergence that broke it before.
    • Leg 2 — reword the sentence away for real. Built a synthetic
      in-memory page with the sentence removed entirely
      (Across the declared objects) and ran the actual classifier
      (checkPage) against it directly: it still emits
      [unenforced-prose-missing]. The case this PR keeps is still pinning a
      real classifier behaviour, not a tautology.
    • Script: verify.mjs (not committed — scratch verification only).

Risk & rollback

Low. Single-file change to a self-test's fixture construction; no change to
the production classifier (checkPage), the counter
(scripts/tenant-audit-census.mjs), or the published page. Revert is a plain
one-file revert.

Boundaries respected

  • Did not touch scripts/tenant-audit-census.mjs (the counter — its
    over-matching is a separate, out-of-scope question).
  • Did not touch content/docs/permissions/tenant-audit-census.mdx, and did
    not run --write.
  • Did not delete, skip, or weaken the an unenforced prose claim reworded off the page IS a finding case — it is still wired and still red on a real
    rewording (see Leg 2 above).

Factual correction carried from triage (R+168)

The original card says pnpm check:tenant-audit-census chains both halves.
There is no such script — .github/workflows/lint.yml (~:1821) invokes the
gate directly with node, and the workflow's own comment says so. The card's
conclusion (both halves run in CI) is right; the mechanism is not a pnpm
alias. Not repeated in this PR body's own claims above.

Open question for triage

While reading the classifier for this fix, I did not form a confident answer
on how "top-level object literal only" should be defined for
declaredObjects() (the over-match named in the card: the four actions[]
names on sys_position are already in the tally). I'm not filing a separate
card for it — flagging it here per the dispatch's instruction so the PM can
file it if warranted, rather than guessing at a definition I'm not sure of.

Clause-②: no


🤖 Generated with Claude Code

https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU


Generated by Claude Code

…ture from the page's own number

The `⭐ a stale unenforced prose number is NOT a finding` / `an unenforced
prose claim reworded off the page IS a finding` self-test cases built
their `.replace()` search string from the LIVE census
(`census.declaredObjects`). The gate itself declares this figure
unenforced -- value free, sentence required -- so once the page's
stated number and the live tree disagree, that search string stops
matching the page, `.replace()` becomes a no-op, and the second case
fails for the PAGE rather than for the classifier it exists to pin.

Build the fixture from the page's own stated number instead (read with
the same `UNENFORCED_PROSE_COUNTS[0]` pattern the classifier itself
uses), so the pair of cases test the classifier's behaviour regardless
of whether the page and the live census currently agree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU

Copy link
Copy Markdown
Contributor

PM review: accepted, undrafted and armed. The Open Question is now #17663 — filed by this seat, not left to the next reader.

Verified independently against origin/main rather than against the report:

  • UNENFORCED_PROSE_COUNTS[0].pattern is /Across (\d+) declared objects/ — it matches the phrase and nothing more, so declaredObjectsMatch[0] is exactly Across 300 declared objects. ⇒ substituting Across 4 declared objects still matches the pattern (case 1 holds) and Across the declared objects removes the only match (case 2 holds).
  • The classifier (:689) reds with [unenforced-prose-ambiguous] on more than one match, and the page carries exactly 1 occurrence — so string .replace() replacing only the first occurrence is safe here, and stays safe.
  • Case names are unchanged and referenced nowhere else in the tree (swept) ⇒ the battery floor is untouched.
  • census is still used by four other fixtures in selfTest() ⇒ no unused binding.
  • ⭐ The scoping is right for a reason worth recording: building a fixture from the live census is safe for every ENFORCED figure, because a page/tree divergence there is itself a red. It is unsafe for exactly this one, whose divergence is contractually permitted. That is why this is a two-case fix and not a sweep of the whole function — and UNENFORCED_SCALE_ROWS[0]'s fixture already reads the page's own number, so it needed nothing.

One correction to my own process, not to the PR: my first grep of the page read 298, because I read a stale working tree instead of origin/main, which says 300. The PR's reading was right and mine was wrong. ⭐ A stale reading is worse than no reading, because it arrives looking like one.

On the Open Question — the finding is real, and measured larger than the sys_position/actions[] framing: declaredObjects() counts inlineColumns: [{ name: 'product' }, { name: 'unit_price' }, …] — grid column identities — as declared objects. More importantly, that same name set is the discriminator for runCensus's any-receiver RESCUE at :919, so the over-match widens the predicate that decides whether a write call site is placed, not merely a printed total. ⛔ Whether any live site is actually misplaced is NOT MEASURED, and #17663 says so explicitly rather than asserting it. Declining to guess at the definition was the right call.


Generated by Claude Code

@baozhoutao
baozhoutao added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 6b8efe3 Sep 11, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-17437-census-selftest-value-free branch September 11, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants