Skip to content

docs(plugin-audit): the published README documents the record-view audit surface that shipped - #9541

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-9517-readme-read-audit
Aug 18, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-9517-readme-read-audit

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #9517

This is the residual half of the card. PR #9531 (53fc09922) landed the urgent half — the SOC 2 / HIPAA / GDPR claim, the 12 fabricated auditService methods, the wrong row shape, the wrong object name and the 6 nonexistent REST routes are all gone from main, and two dependency boundaries were annotated. It correctly refused to document record-view auditing because that work was still an unmerged draft.

Premise re-verified before editing anything

git merge-base --is-ancestor 5126e795d origin/main && echo landed

prints landed. 5126e795d is feat(plugin-audit): record-view auditing — who viewed which record (#8992) (#9515), so the blocker named in the previous round is cleared and the surface is now describable. Everything below was measured against origin/main at 53fc09922, not against the card's description of it.

What the README now documents

  • The read action, in the action table with its writer and trigger, and the record_views list view in the views table.
  • The per-object opt-in as an install-time list, with the constructor spelling and the three settings the plugin forwards.
  • Off-the-request-path batched writes — enqueue-and-return, the size and timer flush thresholds, the destroy() tail drain, and why created_at holds the view instant rather than the flush instant.
  • The record-detail discriminator — one materialized record plus a primary-key pin, the AND-composed predicate the security middleware leaves behind, the $or / $not refusal, and the depth bound. This is what keeps list and search reads out of scope, so it is stated as the scope rule rather than as trivia.
  • Both declared boundaries — a system-elevated read and a read with no principal each write no row.
  • That no field values are recorded, and the consequence: the ledger cannot answer what a viewer actually saw.

Two things only reading the code shows, both now on the page:

  1. maxBufferedEvents (default 10000) is a writer knob the plugin does not forward. Documenting it as a plugin option would have been a small instance of exactly this card's defect.
  2. The shipped record_views view carries an ip_address column that is always empty on a read row, because no read-path writer stamps it. Filed separately as record_views lists an ip_address column that no read-path writer ever stamps — a declared-but-unwritten column on a shipped compliance view #9539; documented here rather than left to surprise a reader.

The opt-in is an install-time list, not a metadata key

The README says so explicitly, and says why. enable.auditReads appears on the page exactly once, in a sentence stating it does not exist — and the verification below asserts that, so a later edit cannot quietly turn the mention into a documented API.

Scope 4 re-checked: record-view auditing introduces no enterprise boundary

packages/spec/src/kernel/platform-capabilities.ts:143 declares audit: { package: '@objectstack/plugin-audit', edition: 'open' }. The read writer lives in this package, the opt-in is ordinary plugin configuration, and nothing about the capability degrades on an open build. ⇒ Nothing new is annotated under the access-recipes.mdx pattern; the page says so in one sentence rather than inventing a dependency. The two existing annotations (archive datasource fails closed to retention, hierarchy resolver fails closed to own) are untouched, and the second is noted as applying to read rows the same way it applies to every other row.

Evidence: a set-equality check anyone can re-run

Matching the bar PR #9531 set. Save and run from the repo root:

cat > /tmp/check-audit-readme.mjs <<'EOF'
import { readFileSync } from 'node:fs';
const P = 'packages/plugins/plugin-audit/';
const readme = readFileSync(P + 'README.md', 'utf8');
const object = readFileSync(P + 'src/objects/sys-audit-log.object.ts', 'utf8');
const index  = readFileSync(P + 'src/index.ts', 'utf8');
const plugin = readFileSync(P + 'src/audit-plugin.ts', 'utf8');
const writer = readFileSync(P + 'src/read-audit.ts', 'utf8');
let bad = 0;
const eq = (label, doc, src) => {
  const d = [...new Set(doc)].sort(), s = [...new Set(src)].sort();
  const missing = s.filter((x) => !d.includes(x)), invented = d.filter((x) => !s.includes(x));
  const ok = !missing.length && !invented.length; if (!ok) bad++;
  console.log(`${ok ? 'OK  ' : 'FAIL'} ${label}: documented ${d.length} / declared ${s.length}` +
    (ok ? '' : `\n     omitted: ${JSON.stringify(missing)}\n     invented: ${JSON.stringify(invented)}`));
};
const section = (from, to) => {
  const a = readme.indexOf(from), b = to ? readme.indexOf(to, a + from.length) : readme.length;
  if (a < 0 || b < 0) throw new Error('section not found: ' + from);
  return readme.slice(a, b);
};
const firstCol = (text) => {
  const sep = text.search(/^\|[-\s|:]+\|\s*$/m);
  const body = sep < 0 ? text : text.slice(text.indexOf('\n', sep) + 1);
  return [...body.matchAll(/^\|\s*`([a-z_]+)`\s*\|/gm)].map((m) => m[1]);
};
eq('action enum', firstCol(section('## What lands on the ledger', '## `sys_audit_log` fields')),
  object.match(/action:\s*Field\.select\(\s*\[([^\]]*)\]/)[1]
    .split(',').map((s) => s.trim().replace(/^'|'$/g, '')).filter(Boolean));
eq('sys_audit_log fields', firstCol(section('## `sys_audit_log` fields', '**Secret masking.**')),
  [...object.matchAll(/^ {4}(\w+):\s*Field\./gm)].map((m) => m[1]));
eq('list views', firstCol(section('| View | Shows |', '\nIndexes are declared')),
  [...object.matchAll(/^ {4}(\w+):\s*\{\n\s*type: 'grid'/gm)].map((m) => m[1]));
const documented = [...section('## Exports', '## License').matchAll(/^export (?:type )?\{([^}]*)\}/gms)]
  .flatMap((m) => m[1].split(',').map((s) => s.trim()).filter(Boolean));
const unresolved = documented.filter((s) => !new RegExp(`(^|[\\s,{])${s}([\\s,}]|$)`, 'm').test(index));
if (unresolved.length) bad++;
console.log(`${unresolved.length ? 'FAIL' : 'OK  '} exports: ${documented.length} documented symbols` +
  (unresolved.length ? `\n     not exported by src/index.ts: ${JSON.stringify(unresolved)}` : ''));
const optRows = [...section('| Option | Default | Meaning |', '\n⚠️ The writer itself')
  .matchAll(/^\|\s*`readAudit\.(\w+)`\s*\|\s*`([^`]*)`\s*\|/gm)].map((m) => [m[1], m[2]]);
eq('readAudit options', optRows.map((r) => r[0]),
  [...plugin.slice(plugin.indexOf('interface AuditPluginReadAuditOptions'),
                   plugin.indexOf('interface AuditPluginOptions'))
    .matchAll(/^\s{2}(\w+)\??:/gm)].map((m) => m[1]));
const defs = Object.fromEntries([...writer.matchAll(
  /^\s{4}(maxBatchSize|flushIntervalMs|maxBufferedEvents) = ([\d_]+),/gm)]
  .map((m) => [m[1], m[2].replace(/_/g, '')]));
for (const [name, doc] of [...optRows, ['maxBufferedEvents', '10000']]) {
  if (!(name in defs)) continue;
  const ok = defs[name] === doc.replace(/[^\d]/g, ''); if (!ok) bad++;
  console.log(`${ok ? 'OK  ' : 'FAIL'} default ${name}: README ${doc} / source ${defs[name]}`);
}
const claimsKey = /`enable\.auditReads`/.test(readme) && !/There is no `enable\.auditReads`/.test(readme);
if (claimsKey) bad++;
console.log(`${claimsKey ? 'FAIL' : 'OK  '} enable.auditReads is named only to say it does not exist`);
console.log(bad === 0 ? '\nALL CHECKS PASSED' : `\n${bad} CHECK(S) FAILED`);
process.exit(bad === 0 ? 0 : 1);
EOF
node /tmp/check-audit-readme.mjs

Output at 4435acf66:

OK   action enum: documented 8 / declared 8
OK   sys_audit_log fields: documented 13 / declared 13
OK   list views: documented 6 / declared 6
OK   exports: 28 documented symbols
OK   readAudit options: documented 3 / declared 3
OK   default maxBatchSize: README 50 / source 50
OK   default flushIntervalMs: README 2000 / source 2000
OK   default maxBufferedEvents: README 10000 / source 10000
OK   enable.auditReads is named only to say it does not exist

ALL CHECKS PASSED

Nothing is documented that the source does not declare, and nothing declared is left out: 8 of 8 action values, 13 of 13 fields, 6 of 6 list views, 28 of 28 export symbols resolving in src/index.ts, 3 of 3 plugin options with their defaults matching the writer's.

The check is proven able to fail

A green check nobody has seen go red is an assurance, not evidence. Three ablations, each run against the committed tree and each restored to byte identity afterwards:

Ablation Result
delete the read row from the action table FAIL action enum: documented 7 / declared 8 — omitted: ["read"]
delete the record_views row from the views table FAIL list views: documented 5 / declared 6 — omitted: ["record_views"]
document maxBatchSize as 25 FAIL default maxBatchSize: README 25 / source 50

After restore, git diff --stat HEAD is empty and the check passes again.

Gates

Derived from git merge-base origin/main HEAD (53fc09922) per #9320, not from a two-dot range: node scripts/pm/dispatch-gates.mjs .changeset/mighty-ducks-repeat.md packages/plugins/plugin-audit/README.md gives 8 path-derived plus 1 convention-triggered. All run at 4435acf66, which is the branch tip and the tree every gate saw.

Gate Result
check:changeset-gate-self-tests OK (118 + 206 + 116 assertions)
check:objectui-changeset OK
check:test-source-alias OK (72 packages)
check:type-source-resolution OK (76 packages)
check-adr-0087-registration.mjs OK (1 non-breaking changeset seen)
check-changeset-no-major.mjs OK
check-empty-changeset.mjs OK (1 declaring changeset added)
check-affected-docs.mjs OK (220 self-test cases)
check:i18n (convention-triggered) OK (9 packages, all bundles in sync)
check:nul-bytes (any edit) OK (6151 files, 0 control bytes)

check:i18n refused the unbuilt tree first (PREREQUISITE NOT MET, exit 1) and only went green after turbo run build --filter=@objectstack/cli (55 tasks). Reported green here is green, not skipped.

Package scope, after building the dependency closure (pnpm --filter '@objectstack/plugin-audit^...' build):

Test Files  17 passed (17)
     Tests  268 passed (268)

and pnpm --filter @objectstack/plugin-audit typecheck clean — the script name is echoed in the output, so this is not a zero-match silent pass. All heavy runs were serialized through flock -E 99 -w 240 /tmp/os-heavy-verify.lock; no queue timeouts.

⚠️ No dogfood ablation is claimed and none applies: the diff is one Markdown file plus a changeset, with zero executable code. The test and typecheck runs are a no-regression control, not evidence about README content — the set-equality check above is that evidence.

Changeset

Owed, patch. PR #9531's reasoning is the precedent and it holds here: README.md is in this package's published files array with private unset, so a docs-only correction with no version bump never reaches the npm package page at all. The changeset is the mechanism that publishes the correction, not paperwork — which is why skip-changeset would be the wrong call.

Findings filed, not fixed here


Generated by Claude Code

…dit surface that shipped (#9517)

PR #9531 corrected this README against the shipped surface while record-view
auditing was still an unmerged draft, so it correctly refused to describe it.
That work has since landed (#8992 via PR #9515), which made two of the page's
statements false: "reads and views are not on the ledger", and that the plugin
takes no configuration.

The page now documents the surface that exists, each point measured against the
source rather than against a description of it: the `read` action and its writer
in the action table, the `record_views` list view, the record-detail
discriminator (one materialized record plus a primary-key pin, `$or`/`$not`
refused) that keeps list and search reads out of scope, the batched
off-request-path writes with the view-instant `created_at` and the two loud
once-only failure postures, and the two declared boundaries — a system-elevated
read and a read with no principal both write no row.

The opt-in is documented as what it is: an INSTALL-TIME list on the plugin
constructor, explicitly not an `enable.auditReads` object-metadata key. That
spelling was ruled against on #8992 for the reason this card exists — a
declarable key can be set on an object in a deployment that never installs the
plugin, producing metadata that reads as audited and writes nothing.

Two things the page now says that only reading the code shows: `maxBufferedEvents`
is a writer knob the plugin does not forward, and the shipped `record_views` view
carries an `ip_address` column that is always empty on a `read` row because no
read-path writer stamps it.

Record-view auditing adds no enterprise dependency — this package's declared
edition is `open` — so nothing new is annotated under that pattern; the two
existing annotations are unchanged.

A changeset is owed because the README ships in the package's `files` array: a
docs-only correction with no version bump never reaches the npm package page.

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-audit/README.md) — pages documenting those are invisible to this run

Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 02ebb6f5b3e43f3878edcdfdb6533aff99c19c4a → packageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 18, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 18, 2026 08:33
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit b348ac2 Aug 18, 2026
25 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-9517-readme-read-audit branch August 18, 2026 08:44
os-warren pushed a commit that referenced this pull request Aug 19, 2026
… replace with actor

sys_audit_log's record_views list view declared an ip_address column that no
read-path writer ever stamps: buildRow in read-audit.ts stamps action,
created_at, user_id, object_name, record_id, old_value, new_value, tenant_id,
and conditionally organization_id/actor -- never ip_address, since client-
fingerprint fields are populated on auth events only. On a compliance
screen an always-empty column reads as "captured, and none" rather than
"not captured" -- the same narrow-not-untruthful defect class #7675/#8147/
#8315 retired from this object's action enum, one layer down on a column.

Replaced with actor, which the read writer DOES stamp on every row and which
attributes a service principal that user_id structurally cannot hold.

Pinned by sys-audit-log-record-views-columns.test.ts: the stamped key set is
derived at runtime from a real engine run of the writer, never hand-copied,
so the class can't regrow silently. Ablated (put ip_address back, confirmed
red, restored byte-identically) per the standing lane clause.

Deleted the one README bullet (from #9517/PR #9541) that documented the
column as always-empty, since it no longer applies.

Maintainer ruling 2026-08-18 + triage auto-adjudication 2026-08-19 (both
Option 1). Stamping viewer IP (Option 2) is explicitly NOT commissioned.

Fixes #9539

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 23, 2026
… replace with actor (objectstack-ai#9956)

sys_audit_log's record_views list view declared an ip_address column that no
read-path writer ever stamps: buildRow in read-audit.ts stamps action,
created_at, user_id, object_name, record_id, old_value, new_value, tenant_id,
and conditionally organization_id/actor -- never ip_address, since client-
fingerprint fields are populated on auth events only. On a compliance
screen an always-empty column reads as "captured, and none" rather than
"not captured" -- the same narrow-not-untruthful defect class objectstack-ai#7675/objectstack-ai#8147/
objectstack-ai#8315 retired from this object's action enum, one layer down on a column.

Replaced with actor, which the read writer DOES stamp on every row and which
attributes a service principal that user_id structurally cannot hold.

Pinned by sys-audit-log-record-views-columns.test.ts: the stamped key set is
derived at runtime from a real engine run of the writer, never hand-copied,
so the class can't regrow silently. Ablated (put ip_address back, confirmed
red, restored byte-identically) per the standing lane clause.

Deleted the one README bullet (from objectstack-ai#9517/PR objectstack-ai#9541) that documented the
column as always-empty, since it no longer applies.

Maintainer ruling 2026-08-18 + triage auto-adjudication 2026-08-19 (both
Option 1). Stamping viewer IP (Option 2) is explicitly NOT commissioned.

Fixes objectstack-ai#9539


Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… list with a counterfactual pin (objectstack-ai#18700)

Fixes objectstack-ai#18560

**Clause-②: no**

`scripts/pm/check-widening-tells.mjs` is the TELL half of the clause-②
gate (the DECLARATION half, `check-clause2-carriers.mjs`, is a different
file and PR objectstack-ai#18681's subject). Its T1 reader decides a line declares a
schema member by what the property's VALUE opens with, and that
vocabulary was five alternatives inside one 130-character regex literal.
A form missing from it is not a line judged leniently — it is a line
that is not a key line at all: `memberTellKind` answers `null`, the row
neither fires nor spends the objectstack-ai#16943 replacement budget nor earns it on
the removed side, and nothing in the output says so. The silence is
indistinguishable from a correct `no`.

## The counterfactual, before and after

Re-derived here rather than inherited from the card's reading
(objectui#9647 comment 5707064702, an at-tier reviewer's measurement).
Run as `PM_SWEEP_REPO=objectstack-ai/objectui node
scripts/pm/check-widening-tells.mjs --declaration no --diff …`, the
BEFORE leg against the file as it stands at `6dfa3ea77` (this branch's
merge base), the AFTER leg at `7f1a81419`:

| diff | declaration | BEFORE (6dfa3ea) | AFTER (7f1a814) |
|---|---|---|---|
| objectui#9647, unmodified | `no` | exit 0, 1 file judged, **0 tells**
| exit 0, 1 file judged, 0 tells |
| objectui#9647 plus one key added through `stripImportedDefaults(` |
`no` | **exit 0, 0 tells** | **exit 4, T1 at
`packages/types/src/zod/data-display.zod.ts:584`** |
| the same widened diff | `yes` | exit 0 | exit 0 (a `yes` is never
blocked) |

Row 2 is the red this PR turns. Row 1 does NOT change verdict, and that
is the correction the re-derivation forced.

## What the re-derivation corrects

The two helpers the card names are objectui's REFUSAL family
(`packages/types/src/zod/tombstone.zod.ts`), read off the source:

- `retirementTombstone(guidance)` returns `z.never({ error: guidance
}).optional().describe(guidance)` — the same primitive as this repo's
`retiredKey()`;
- `handlerKeyRefusal(key, disposition, label)` returns a `z.custom`
predicate typed `never` that returns `false`, and its own docblock
records that "The predicate refuses EVERYTHING, a live function
included".

A key declared through either is a key an author may NO LONGER write.
Making those two fire a tell would re-mint, on 290 objectui key lines,
the exact false positive objectstack-ai#17955 removed on 255 objectstack ones — and a
false tell does not cost a word in a comment, it costs the false `yes`
this file's own header refuses to ask an author for.

The form that DOES carry a widening, and that no seat had named, is
`stripImportedDefaults()`
(`packages/types/src/zod/imported-defaults.ts`), whose docblock states
its contract as "the same TypeScript type, the same keys, the same
checks, the same registry metadata and the same accept set". It returns
a LIVE schema and is spelled at 45 key positions on the judged objectui
surface.

## The named list

`SCHEMA_PROPERTY_FORMS` — exported, frozen, and the constant
`SCHEMA_PROPERTY` is BUILT from it. Two fields carry two questions:
`pattern` (what makes the line a KEY LINE) and `writable` (whether the
key it declares is one an author may write). Counts are key-POSITION
counts, each with the tree it was taken against.

| form | writable | measured |
|---|---|---|
| `z.` | yes | 7,784 at objectstack `6dfa3ea77` · 1,482 at objectui
`15f01223d` |
| `lazySchema(` | yes | 0 at objectstack `6dfa3ea77` (live at
DECLARATION positions) |
| `strictObject(` | yes | 47 at objectstack `6dfa3ea77` |
| `*Schema` | yes | 995 at objectstack `6dfa3ea77` · 56 at objectui
`15f01223d` |
| `stripImportedDefaults(` | yes | **45 at objectui `15f01223d`**
(added) |
| `retiredKey(` | no | 255 at objectstack `6dfa3ea77` |
| `retirementTombstone(` | no | **187 at objectui `15f01223d`** (added)
|
| `handlerKeyRefusal(` | no | **90 at objectui `15f01223d`** (added) |
| `aliasKeyRefusal(` | no | **13 at objectui `15f01223d`** (added) |

The `writable: false` arm is objectstack-ai#17955's decline generalised from one
helper name to the family, on the SAME positive, line-local evidence:
the value must BE the call and nothing after it.
`declaresRetiredKeyTombstone` is renamed `declaresUnwritableKey`
accordingly; it is not imported by any other file.

**The `no` criterion is not loosened anywhere, and the direction is
provable rather than argued.** An unrecognised line reports NOTHING, so
no row that fires today can stop firing when the list grows. A self-test
case keeps the literal this replaced as the reference and asserts it:
every legacy verdict is byte-identical, and the only cells that move are
the four added forms moving from "not a key line" to "a key line" — one
direction, zero losses.

## The pins

New battery, registered on the roster and pinned: `'objectstack-ai#18560 — the
declaring vocabulary is a NAMED list, every form pinned by a
counterfactual fixture': 30` (39 cases run against a floor of 30). Its
unit is the FORM, not the assertion:

- a frozen fixture roster is asserted EQUAL to the form set, in both
directions and naming the offenders — a form added to the list without a
fixture reds, a form silently dropped from the list reds. The fixtures
are deliberately NOT generated from the list, which would make every
future form pass by construction;
- every form is asserted RECOGNISED (`memberTellKind` answers T1) —
writable or not, because an unrecognised line is invisible to both sides
of the budget, which is the defect itself;
- every form's fixture is then driven through `tellsInFile` and asserted
against its OWN register: a `writable` form must FIRE with its
file:line, an unwritable one must be recognised and DECLINE;
- every unwritable form carries the chained-arm control that FIRES, so
the decline is bound to the evidence the line carries and never to the
helper's name;
- the objectui#9647 shape is carried as a case in all three readings,
plus the `stripImportedDefaults(` widening with its refusal, its
file:line, the `yes` control and the default-board control.

`check:entry-guard` is the reason the vocabulary's structural guard is a
const initializer's `map` step rather than a top-level `for`: this file
exports bindings the sibling gate imports, so a load-time throw would
run inside the importer.

## The census — report-only, no re-grade, no state write

Has the silence already been relied on? **Zero confirmed `Clause-②: no`
landings through these forms, over 11 of 18 rows read.**

- **Horizon.** objectui's full history (`git rev-parse
--is-shallow-repository` = false, 10,282 commits, initial commit
2026-01-13) up to the checkout tip `15f01223d` (2026-09-16). PR
objectui#9647 itself is OUTSIDE it: `git merge-base --is-ancestor
604476d HEAD` exits 1, with the initial commit as the control leg at
exit 0 on the same non-shallow checkout. The judgeable part of that
window starts 2026-09-10, when objectstack-ai#17278 first let this CLI be told which
board it judges.
- **Population.** 46 commits add a key through one of the four added
forms on `packages/types/src/zod/**`; 18 land inside the judgeable
window.
- **Coverage — read (11 of 18):** 10 carry `Clause-②: yes` in the PR
body (objectui#8884, objectstack-ai#8895, objectstack-ai#8930, objectstack-ai#8967, objectstack-ai#9051, objectstack-ai#9338, objectstack-ai#9495, objectstack-ai#9539,
objectstack-ai#9541, objectstack-ai#9565); 1 (objectui#9443) carries a "Clause-② carriers" section
attaching `needs:contract-review` with no `yes`/`no` token.
- **Coverage — NOT ATTEMPTED (7 of 18):** objectui#8984, objectstack-ai#9254, objectstack-ai#9261,
objectstack-ai#9343, objectstack-ai#9544, objectstack-ai#9589, objectstack-ai#9621 carry no declaration in the PR body; the
remaining carrier is each card's claim comment, which was not read.
**This is NOT a zero** — it is seven rows unread and named.
- The commit-local changeset is a weak carrier in objectui: only 2
commits in the whole history carry a `Clause-②` line in a changeset,
which is why the census reads PR bodies rather than the tree.

## The ablation

From the committed fix, `HEAD` blob
`1b741ee6c22db6523475b698d392a8ecd41686d6`, under a `trap '…' EXIT INT
TERM` restoring `git checkout HEAD --
scripts/pm/check-widening-tells.mjs` at an absolute path. No build or
`dist/` is involved — the gate runs from source, so there is no rebuild
leg to prove.

- **mutation:** the `retirementTombstone(` and `handlerKeyRefusal(` rows
deleted from `SCHEMA_PROPERTY_FORMS`;
- **mutation proved on disk, not from an exit code:** anchor counts 1 →
0 for each form, file 257,365 → 256,697 bytes, blob
`9fb65edf6f68b1501a0aec29d0ddb4c95bcf44a2` which is not the HEAD blob;
- **predicted direction:** RED. **Observed:** RED — `--self-test` exits
1, "6 of 377 case(s) failed" plus the verdict-handshake report. The
failures name the mechanism: the orphaned-fixture case prints both
dropped forms, both RECOGNITION cases fail, both objectui#9647 cases
fail, and the one-direction case fails;
- **restore verified by hash, not by a return code:** `git hash-object`
back to `1b741ee6c22db6523475b698d392a8ecd41686d6`, `git diff HEAD`
empty, `git status --porcelain` clean, and the suite back to 381 cases
pass.

## Self-test

`pnpm check:pm-widening-tells` — **381 cases pass** (342 before this PR;
+39). Exit 0. The two gates that IMPORT this module were run too:
`check:pm-clause2-carriers` exit 0, `check:pm-prior-rulings` exit 0.

## Derived gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed path list —
29 commands, each run with `$?` captured BEFORE any pipe. Reconciled
with `--ran`: "29 derived famil(ies) accounted for — 29 run, 0
NOT-MEASURED (a DERIVED zero — all 29 recorded an exit code and none of
them is 3)".

```
exit 0  node scripts/check-ci-filter-parity.mjs
exit 0  node scripts/check-closing-keyword-parity.mjs
exit 0  node scripts/check-closing-keyword-parity.mjs --self-test
exit 0  node scripts/check-comment-mask-corpus.mjs
exit 0  node scripts/check-declaration-mirrors.mjs
exit 0  node scripts/check-declaration-mirrors.mjs --self-test
exit 0  node scripts/check-scripts-symbol-anchors.mjs
exit 0  node scripts/check-scripts-symbol-anchors.mjs --self-test
exit 0  node scripts/check-self-test-wired.mjs
exit 0  node scripts/check-self-test-wired.mjs --self-test
exit 0  node scripts/check-self-test-workflow-commands.mjs
exit 0  node scripts/check-self-test-workflow-commands.mjs --self-test
exit 0  node scripts/check-whole-set-label-write.mjs
exit 0  node scripts/check-whole-set-label-write.mjs --self-test
exit 0  node scripts/pm/bare-root-worklist.mjs --self-test
exit 0  pnpm check:agent-test-spelling
exit 0  pnpm check:bash32-floor
exit 0  pnpm check:cli-command-ids
exit 0  pnpm check:cross-package-test-inputs
exit 0  pnpm check:driver-memory-census
exit 0  pnpm check:entry-guard
exit 0  pnpm check:nul-bytes
exit 0  pnpm check:parse-guard
exit 0  pnpm check:pm-dispatch-gates
exit 0  pnpm check:pm-widening-tells
exit 0  pnpm check:pnpm-filter-targets
exit 0  pnpm check:ratchet-remedy-authority
exit 0  pnpm check:refd-timer-probe
exit 0  pnpm check:watch-hint-literal
```

Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `7f1a81419`:
**exit 0**, no findings. Control-byte scan over the edited file: no
hits.

`skip-changeset`: `scripts/pm/**` is not published by any package's
`files[]`, and this diff touches nothing else.

## One boundary this does NOT close, recorded rather than left to be
found

A FILE-LOCAL declaring factory. Both trees mint them —
`placeholderFree(` (23 key lines), `strictIdent(` (12), `emptyProps(`
(9) at objectstack `6dfa3ea77`; `chatbotRequestBodyArm(` (2),
`retiredDeclarativeKanbanKey(` (1) at objectui `15f01223d` — and a list
of shared, exported helpers cannot name a factory private to one file. A
name-shaped heuristic is refused in the header, with the overturn
condition written down. Filed as its own finding in the report on the
card, not fixed here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

2 participants