fix(plugin-audit): report a lost audit row once per CAUSE, and name the real cause in the first line - #17450
Conversation
…he real cause in the first line `reportAuditWriteFailure` deduped on one process-wide boolean, so after the first failure of ANY cause every later failure of every OTHER cause degraded to `debug` for the life of the process, and the one `error` line it did print named the telemetry-datasource remedy unconditionally — the measured `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` refusal sent its operator to check a datasource that was working. The dedupe key is now the failure's identity — the error `code` (or its absence) plus the object. A repeat of an already-reported cause still degrades to `debug`; a new cause reports at `error`, once. The key is built from the `code` and never the message, which is what keeps the cause set bounded by the boot-declared object registry and the driver's code vocabulary rather than by traffic. The ADR-0057 §3.6 datasource guidance is kept and made conditional on the missing-table cause it is the remedy for, asked through the shared `isMissingTableError` predicate for both tables `persistAuditTrailRow` writes. Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW Co-authored-by: Claude <noreply@anthropic.com>
`pnpm check:type-source-resolution` reds on the new dependency: without a `paths` rule this package's typecheck would be a verdict about `types/dist` build state rather than about the checkout. Same entry, same spelling, same reasoning `plugin-security` records for the identical import. Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b901b4d3392ca36f0434c5336c20291301cae3f1 && git checkout b901b4d3392ca36f0434c5336c20291301cae3f1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab56ea3a1ea6fde6161781c93a013fd494d39193 8f683e2784b016f20326c0e2fa50b7a87ee8e3ee && git checkout -B drift-repro ab56ea3a1ea6fde6161781c93a013fd494d39193 && git merge --no-ff 8f683e2784b016f20326c0e2fa50b7a87ee8e3ee
node scripts/docs-audit/affected-docs.mjs --json ab56ea3a1ea6fde6161781c93a013fd494d39193
|
Fixes #15166
Clause-②: no
(declaration line written by the
domain:servicesreview seat, not by the implementer — theClause-②:line is the claim's, andCheck Changesetreads it only as a line-initial literal in this body. Measured on THIS diff, not predicted from the card: 0 added lines containingexport, 0 newERROR_CODE_LEDGER/code:entries, 0 addedthrows. The change imports an existing predicate (isMissingTableError) and alters a log line's wording plus an in-process dedupe key — no accept set, no published symbol, no error code, no wire field. Thepatchlevel stands. ⛔ Do not delete this line; the gate reads the body, so removing it re-reds the PR.)reportAuditWriteFailure— the best-effort catch aroundpersistAuditTrailRow— deduped on one process-wide boolean. Two consequences, both measured onorigin/mainbefore writing a line:debugfor the life of the process.persistAuditTrailRowis registered inDURABILITY_CRITICAL_CALLEESprecisely because a lost audit row must be reported aterror.errorline it did print named the telemetry-datasource remedy unconditionally, never readingerr— so theERR_SYSTEM_WRITE_ORGANIZATION_REQUIREDrefusal measured on Asys_audit_logwrite refused by the system-write organization rule is swallowed by plugin-audit's best-effort catch, so the audit row about a defective record is LOST silently — surfaced by #13636's admission #14927 answered "check the datasource", sending the operator to inspect something that was working.What changed
The dedupe key is now the failure's identity — the error
code(or its absence) plus the object — instead of one process-wide boolean. A repeat of an already-reported cause still degrades todebug, exactly as before; a new cause reports aterror, once.The first
errorline now leads with the underlying code and message. That information was already computed one line above the branch (const detail = …) and passed only into thedebugpayload; nothing new is plumbed in to reach it.The ADR-0057 §3.6 datasource guidance is kept, not deleted — it is the correct remedy for the "no such table" cause it was written for — and is now printed only for that cause, decided by the shared
isMissingTableErrorpredicate rather than a hand-rolled second vocabulary. It is asked about both tablespersistAuditTrailRowwrites (sys_audit_logand itssys_activitymirror), since ADR-0057 §3.6 routes both.The premise this hung on, and how it came out
The direction on this card was conditional on falsifying one premise first:
The premise HOLDS. The named failure mode — a cause set that is unbounded or high-cardinality turning "one
errorper cause" back into a flood — did not materialise, because the key is built from thecodeand never the message. Measured against the real code path, drivinginstallAuditWritersover the showcase reference install's audited object set (48 platform + 24 showcase objects declared, minus the 7 ofSKIP_OBJECTSdeclared there = 65 audited objects):errorlines BEFOREerrorlines AFTERerror")H1/H2/H3 are the named hazard — thousands of failures each carrying a distinct, row-naming message — and the
errorcount did not move with them. The decisive reading is traffic-independence:errorlinesFour times the traffic, identical cause set. The reported-cause set is bounded by two sets fixed at BOOT — the declared object registry and the driver's code vocabulary — and by nothing that grows with traffic. That is the property the anti-noise rule needs; #4420's shape was unbounded per-write growth, which the falsifier still is and this is not.
The residual, stated honestly: for ONE systemic cause spread across the whole audited object set, the operator now gets up to one line per audited object instead of one line total — measured 65 in the showcase reference install. Each is printed once and never again. This is the cost of the ruling's
(code, object)granularity, and it is bounded; the falsifier would have printed 6,500 and kept going.Ablation
The keying was reverted to the process-wide boolean on the committed tree, with the mutation proved on disk before the run (occurrence counts and
git hash-objectboth moved), and the new pins went RED:Restoration proved BY STATE, not by exit code:
git diff HEADempty,git status --porcelainempty, andgit hash-objectback to379e7067ca053dff15eb50c56a16a817e05ed526— the HEAD blob.Verification
Measured at
8f683e278.pnpm --filter @objectstack/plugin-audit test— 334 passed / 23 files, exit 0 (67 pre-existing cases inaudit-writers.test.tspass unchanged; 8 new).pnpm --filter @objectstack/plugin-audit typecheck— exit 0.pnpm check:durability-log-level— exit 0; its own verdict line:✓ durability-degradation log levels: 29 durability-critical catch seam(s), all loud, rethrowing or propagating to the caller. ⛔DURABILITY_CRITICAL_CALLEESis untouched.pnpm --filter @objectstack/types exec vitest run src/driver-error-classification.callers.test.ts— exit 0. The newisMissingTableErrorcall joins that repo-wide scan's population and names the object it read, as Nothing stops an in-repo caller from callingisMissingTableErrorwithout its read-table argument — and the silent result is the wide verdict #13324 just removed #13440 requires.dispatch-gates.mjs, re-derived after the change set moved): 74 derived, 71 run green, 3 NOT MEASURED, 0 unrun.pnpm lint(eslint . --no-inline-config, whole repo) — exit 0.NOT MEASURED:
check:dual-build-cjs-loads,check:i18n,check:type-check-debt— all three exited 3 (PREREQUISITE NOT MET), one cause: they read built output across the whole workspace and no fulldist/closure exists locally. That is a farm-wide build, and it belongs to CI. ⛔ Recorded as not measured, never as a pass.Deviations from the dispatched file surface
Two files outside the declared surface (
audit-writers.ts+ test siblings), both inside this package, both forced rather than chosen:packages/plugins/plugin-audit/package.json— adds@objectstack/types(workspace), forisMissingTableError. AGENTS.md is explicit that a hand-rollederr.code === '42P01'is the wrong route and the shared predicate is the right one; the predicate is not reachable from this package's existing dependencies and neither@objectstack/corenor@objectstack/objectqlre-exports it. No transitive edge is added —corealready depends ontypes.packages/plugins/plugin-audit/tsconfig.json— apathsrule for that dependency.pnpm check:type-source-resolutionwent RED on the new import (exit 1) and prescribed exactly this: without it, this package's typecheck would be a verdict abouttypes/distbuild state. Same entry, spelling and reasoningplugin-securityrecords for the identical import. The gate is green after it.auth-event-audit.tswas not touched — see the sibling finding below.Sibling finding — not fixed here
auth-event-audit.tscarries a second, independent copy of this shape, not a shared reporter: its ownlet failureReported = falseand its ownreportAuthEventWriteFailure, with its own message literal. Its docblock says so in as many words — "Same discipline, and the same reason, asreportAuditWriteFailureinaudit-writers.ts". Both defects this PR fixes are present there verbatim: a process-level boolean, and an unconditional datasource remedy. Widening this PR to cover it was out of scope per the dispatch, so it is reported rather than fixed.Out of scope, unchanged
The tenancy refusal path for
sys_audit_log(ADR-0131 D7; #14927 closed); any change toDURABILITY_CRITICAL_CALLEES; and "log every failure aterror", the named falsifier, which this is not.Generated by Claude Code
Generated by Claude Code