Repository navigation
fix(plugin-audit): the Audit write FAILED line names the refused table and the row it lost - #21383
Conversation
…e and the row it lost The line read the refused table from a list and always said the sys_audit_log row was lost, even when the refused insert was sys_activity and every ledger row landed. It now names the table persistAuditTrailRow had in flight when it threw and the row that is lost. The missing-table remedy names both causes the evidence cannot tell apart (schema sync never created the table, or the datasource split) in the order to check them. The once key gains the refused table, and the sentence states the key: once per audited object, refused table and error code. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…er cause and the once key Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… table Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 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 cc336cc723665ca92becf487c48d9ac0ab813a02 && git checkout cc336cc723665ca92becf487c48d9ac0ab813a02
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee75aae1a8ebc2625e57074527d446151535634b ff4678856ff5aa91e57884a5790c81f2de349829 && git checkout -B drift-repro ee75aae1a8ebc2625e57074527d446151535634b && git merge --no-ff ff4678856ff5aa91e57884a5790c81f2de349829
node scripts/docs-audit/affected-docs.mjs --json ee75aae1a8ebc2625e57074527d446151535634b
|
… activity row Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21262
Clause-②: no
What changed
reportAuditWriteFailureinpackages/plugins/plugin-audit/src/audit-writers.tsprints theerrorline for a refused audit-trail insert. The record writer stores thesys_audit_logrow that records who did it, then, when activities are enabled and the write has one, itssys_activitytimeline row.persistAuditTrailRownow moves a progress marker forward before the activity insert, so after a throw the caller knows which insert was refused. The line opensAudit write FAILED on TABLEand names that table. The writer decides the table. The table is no longer picked from a list.sys_activityinsert says the ledger row landed and only the activity row is lost. Its header reads "the activity timeline is now INCOMPLETE". A refusedsys_audit_loginsert says the ledger row is lost. When the object writes an activity row, it says that row is lost too, because it is written only after the ledger row.42P01with no phrase naming a relation answers "missing" for any table name. Asked in list order, it would have namedsys_audit_logfor a refusedsys_activityinsert. For a missing table, the line no longer asserts the datasource split. It names both causes the evidence cannot tell apart, in the order to check them: (1) schema sync never created the table, so look forSchema sync FAILED for object 'TABLE'in the boot log; (2) otherwise the ADR-0057 §3.6 telemetry-datasource split, withOS_TELEMETRY_DB=0. Any other cause keeps the driver-fault remedy unchanged.debugrepeat and theerrormeta carrytable.auditFailureCauseKeytakes an optional third argument for the table. The two single-table sinks (auth-event-audit.ts,read-audit.ts) do not pass it, and their keys are byte-identical to before. Neither sibling file is touched.Measured on
main, then on this branchFixture: a MySQL-shaped refusal (
ER_NO_SUCH_TABLE, errno 1146,Table 'objectstack.TABLE' doesn't exist) thrown by that table's owncreate. Four audited objects, three writes each. The fixture was a temporary test file, run and then deleted, never committed.On
mainatf39760864, refused insert intosys_activity: 4errorlines, 8debuglines, and 12sys_audit_logrows landed. The first line:On
main, refused insert intosys_audit_log: the same text with only the quoted driver message changed. 4errorlines, 8debuglines, and no rows landed in either table.On this branch, refused insert into
sys_activity: still 4errorlines and 8debuglines, and 12sys_audit_logrows landed. The first line:On this branch, refused insert into
sys_audit_log: the line opensAudit write FAILED onsys_audit_log, keeps "the compliance trail is now INCOMPLETE", and says that thesys_audit_logrow never landed, "and neither did itssys_activitytimeline row, which is written only after it". The remedy namessys_audit_login both causes.The once key: kept per audited object, with the table added
reportAuditWriteFailurereports once per process then degrades todebug, and its first line blames the telemetry datasource for every repeated cause — an operator is misdirected on the second loss of any kind #15166's granularity, and that card's pinseparates causes per OBJECT as well as per codeholds two objects failing the same way to two lines. The direction says finding(plugin-audit):reportAuditWriteFailurereports once per process then degrades todebug, and its first line blames the telemetry datasource for every repeated cause — an operator is misdirected on the second loss of any kind #15166's pins stay green. A per-table key would turn that pin red.main. The line now states this count where it used to say "reported ONCE".Why the missing-table remedy names two causes
The writer holds the error and nothing else. The boot reports a refused DDL through objectql's schema-sync report (
Schema sync FAILED for object 'X'), but nothing records it where this writer can read it. The engine's datasource binding resolver is private. Telling the two causes apart would need a new seam in@objectstack/objectql, which is outside this card's file surface. So the line names both. The boot-log check comes first because it settles the question whenever that line is present.Pins
These are in
audit-writers.test.ts, blockthe line names the refused table and the lost row (#21262), with 9 cases. The engine refuses one table's insert from that table's owncreate.sys_activity: the line namessys_activity, says the ledger row landed, and never says thesys_audit_logrow was lost. It also checks the rows that actually landed.sys_audit_log: the line names it and says the activity row due after it was lost too.enable.activities: false: no activity row is claimed as lost.42P01on a refusedsys_activityinsert:sys_activityis named. This guards against picking the table by list order.errorlines and 8debuglines, and the line states its key.The
#5226and#15166blocks are byte-unchanged. The test diff is +232 / -0, and all of their pins pass.Ablations (one-time proof, not kept)
Each one was run through
scripts/ablation-replace.mjsin wrap mode, under the verify lock, on the committed fix. The test imports./audit-writers.js, which resolves straight tosrc/audit-writers.ts. No packageexportsordist/is in the path, so no rebuild leg and no dist preflight was owed.table: progress.writing,replaced bytable: 'sys_audit_log' as AuditTrailTable,437819f06fb9to0e5e65f028f2sys_activityline)auditFailureCauseKey(object, err, table)replaced byauditFailureCauseKey(object, err)437819f06fb9tob89d418cd559sys_audit_logis asked first)437819f06fb9to119abca80eeaEvery restore was proven the same way: the blob after restore equals the blob at HEAD (
437819f06fb9), andgit diff HEADis empty.Verification, at
743fc4e5epnpm --filter @objectstack/plugin-audit test: 36 files, 581 tests passed.pnpm --filter @objectstack/plugin-audit typecheck:tsc --noEmitfor src and fortsconfig.scripts.json, pluscheck:test-typecheckovertsconfig.test.json, which compiles the test files. All green.packages/typesdriver-error-classification.callers.test.tsreads everyisMissingTableErrorcall underpackages/, so it reads this file: 7 of 7 passed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 65 commands, and all 65 exited 0.check:dual-build-cjs-loadsandcheck:i18nfirst answeredPREREQUISITE NOT MET(exit 3, nothing measured). After the turbo builds they name (all cached), both re-ran with exit 0. Reconciling with--rangives 65 derived, 65 run, 0 NOT-MEASURED. That zero is derived from the recorded exit codes.check:durability-log-level(39 durability catch seams, all loud),check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check:startup-registry-verdict.check:doc-authoring: the runtime string keeps its oneframework#5226id, so the sibling-package prose-id ledger holds, with no growth and no burn-down.--no-inline-config --format json: 2 files linted, 0 errors, 0 warnings. Both files resolve a non-empty config under--print-config, so they are in eslint's own population. The config never enables type-aware linting (parserOptions.projectandprojectServiceare null for both), so this diff cannot move a verdict on any untouched file. The repo-widepnpm lintis left to CI.Acceptance notes (not filed)
read-audit.ts(Read-audit write FAILED) andauth-event-audit.ts(Auth-event audit write FAILED) write onlysys_audit_log, so the table they name is right. For a missing table, their remedy still says the datasource split is the usual cause, and a refused DDL gets the same misdirection there. Nobody has measured a missingsys_audit_logtable through either seam, so this is a note, not a card.content/docs/kernel/runtime-services/audit-service.mdx("The most common cause of a failing insert is a datasource split") describes the auth-event seam and carries the same claim. This PR makes none of those sentences false, so they stay as they are. Carrier: none.#5226was a ledger insert riding a primary-datasource transaction. The engine'senforceTransactionOriginnow runs system-ledger writes inside a transaction outside it, on their own connection. That route to "no such table" looks closed, but this is inference from reading the code. The line keeps the split as cause (2) because the writer cannot rule it out.see framework#5226. Removing it would shrinkscripts/doc-authoring-prose-id.baseline.jsonby one, and that file is outside this card's file surface.Generated by Claude Code