docs(plugin-audit): the published README documents the record-view audit surface that shipped - #9541
Merged
Conversation
…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>
Contributor
📓 Docs Drift CheckNothing 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
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Aug 18, 2026
os-project-manager
marked this pull request as ready for review
August 18, 2026 08:33
This was referenced Aug 18, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fabricatedauditServicemethods, the wrong row shape, the wrong object name and the 6 nonexistent REST routes are all gone frommain, 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
prints
landed.5126e795disfeat(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 againstorigin/mainat53fc09922, not against the card's description of it.What the README now documents
readaction, in the action table with its writer and trigger, and therecord_viewslist view in the views table.destroy()tail drain, and whycreated_atholds the view instant rather than the flush instant.$or/$notrefusal, 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.Two things only reading the code shows, both now on the page:
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.record_viewsview carries anip_addresscolumn that is always empty on areadrow, because no read-path writer stamps it. Filed separately asrecord_viewslists anip_addresscolumn 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.auditReadsappears 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:143declaresaudit: { package: '@objectstack/plugin-audit', edition: 'open' }. Thereadwriter 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 theaccess-recipes.mdxpattern; 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 toown) are untouched, and the second is noted as applying toreadrows 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:
Output at
4435acf66: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:
readrow from the action tableFAIL action enum: documented 7 / declared 8 — omitted: ["read"]record_viewsrow from the views tableFAIL list views: documented 5 / declared 6 — omitted: ["record_views"]maxBatchSizeas 25FAIL default maxBatchSize: README 25 / source 50After restore,
git diff --stat HEADis 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.mdgives 8 path-derived plus 1 convention-triggered. All run at4435acf66, which is the branch tip and the tree every gate saw.check:changeset-gate-self-testscheck:objectui-changesetcheck:test-source-aliascheck:type-source-resolutioncheck-adr-0087-registration.mjscheck-changeset-no-major.mjscheck-empty-changeset.mjscheck-affected-docs.mjscheck:i18n(convention-triggered)check:nul-bytes(any edit)check:i18nrefused the unbuilt tree first (PREREQUISITE NOT MET, exit 1) and only went green afterturbo 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):and
pnpm --filter @objectstack/plugin-audit typecheckclean — the script name is echoed in the output, so this is not a zero-match silent pass. All heavy runs were serialized throughflock -E 99 -w 240 /tmp/os-heavy-verify.lock; no queue timeouts.Changeset
Owed,
patch. PR #9531's reasoning is the precedent and it holds here:README.mdis in this package's publishedfilesarray withprivateunset, 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 whyskip-changesetwould be the wrong call.Findings filed, not fixed here
record_viewslists anip_addresscolumn that no read-path writer ever stamps — a declared-but-unwritten column on a shipped compliance view #9539 —record_viewsdeclares anip_addresscolumn that no read-path writer stamps, so it is empty on every row that view can show. Same defect class as the audit-log-browser: 4 of 10 declared sys_audit_log actions have no writer anywhere (login/logout/permission_change/config_change/export/import never materialize) #7675 / audit-log (C): retireexport/import/permission_changefrom thesys_audit_logaction enum and its in-repo consumer surfaces (ADR-0087 registration) #8147 /sys_audit_log.actiondeclaresrestorewith no writer anywhere —actionFor()structurally cannot return it, while a comment and a shipped list view both assert it is covered #8315 enum retirements, one layer down. Not fixed here: it is a code change to a landed feature and does not meet the in-place bar for a docs-only PR.content/docspage — the shipped compliance capability is documented only in a package README #9540 (finding) — record-view auditing has nocontent/docspage; the only written scope is this README. Recorded, not claimed as a defect:check:affected-docsis green, and "the package README is the right home" is a legitimate disposition.Generated by Claude Code