Skip to content

fix(plugin-audit): a failed boot provisioning of an audit table is logged at error, naming what is lost and the fix - #22981

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22913-audit-ddl-error-level
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22913-audit-ddl-error-level

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22913
Clause-②: no

What this changes

AuditPlugin.provisionSystemTables (packages/plugins/plugin-audit/src/audit-plugin.ts) creates the storage for sys_audit_log, sys_activity, sys_comment and sys_comment_reaction at kernel:ready. When the driver refused one of those DDL statements, the catch logged it at warn and continued. The boot completes and the deployment looks healthy while the table does not exist. For the two audit tables, audited writes keep succeeding while their rows have nowhere to land. AGENTS.md "Degradation log levels" sends that class to error.

  • The catch now logs one error per object per boot, never per write. Its first line names the object, what is lost without it, that the deployment keeps looking healthy, and the fix: resolve the driver's refusal, then restart (this provisioning runs on every boot) or run os migrate apply. The driver's refusal is in the record's meta ({ object, error }), not in the message, so a multi-line driver error stays one record.
  • The boot is not failed, per the triage direction (comment 6116902332): making a boot refuse is a separate policy call.
  • The provisioning docblock loses a stale claim, "lazy-created on first WRITE": a write to a missing table fails with no such table (measured below).
  • Ride-along, test-only: plan.telemetry-sibling.integration.test.ts now carries the serve child's own stderr and stdout in every assertion that reads what the boot provisioned. Before, a boot that exited 0 with a table missing printed nothing of its own account.

What a forced refusal of sys_activity's DDL prints now, through os serve (OS_MIGRATE_AND_EXIT=1, OS_LOG_LEVEL=warn, telemetry sibling on), once, with exit 0:

ERROR AuditPlugin: could not provision sys_activity storage — its storage was NOT created. Audited writes keep succeeding, but their `sys_activity` rows (a record's activity timeline and the recent-activity feed) cannot land, and nothing retries them. The boot still completes and the deployment keeps looking healthy. Fix: resolve the driver's refusal (in this record's meta), then restart, since this provisioning runs on every boot, or run `os migrate apply`, which creates the missing storage (`os migrate plan` lists it as `create_table:sys_activity`). If the boot log also carries `Schema sync FAILED for object 'sys_activity'`, that is the same refusal. {"object":"sys_activity","error":"CREATE VIEW \"sys_activity\" AS SELECT … - there is already an index named sys_activity"}

On main the same boot printed WARN AuditPlugin: could not provision sys_activity storage — … inside the Boot diagnostics — N informational block.

Measured before writing the fix half (dispatch A3)

Harness: a scratch app (requires: ['auth', 'automation']), booted through os serve with OS_MIGRATE_AND_EXIT=1. The DDL refusal was forced by pre-creating an INDEX named sys_activity: SQLite tables, views and indexes share one namespace, so the CREATE is refused. Run on main 1f312fdfa code, in both compositions (OS_TELEMETRY_DB=0, and the default telemetry sibling).

step reading
boot with the refusal serve exits 0; the sys_activity view is absent; the audit write that follows logs no such table: sys_activity
fault removed, os migrate plan --json exit 0, pending = ["create_table:sys_activity"]
os migrate apply --yes --json (on a copy) exit 0, created = sys_activity; the view exists afterwards (in the sibling file when the sibling is on)
a plain restart (on another copy) exit 0; the view exists afterwards
OS_SKIP_SCHEMA_SYNC=1 boot of an existing schema with sys_comment_reaction dropped objectql logs Skipping schema sync … WITHOUT DDL, and the table is re-created anyway: this provisioning issues its DDL whatever that flag says

So the line names restart and os migrate apply, which were both measured to restore the table. It does not name OS_SKIP_SCHEMA_SYNC: setting that flag does not stop this DDL, so it cannot make this failure deliberate.

Measured: is the same failure reported elsewhere? (dispatch A2)

The line is printed once per object per boot (kernel:ready fires once, and the loop visits each object once). In both compositions above, objectql's boot schema sync had also tried the same object, so the boot log also carries ERROR Schema sync FAILED for object 'sys_activity'. It carries it twice, from two sync passes in that boot, each with its own finished with 1 FAILED object(s) summary. The audit writer's per-write line (Audit write FAILED on sys_activity, printed once per audited object, table and code) follows. The new line says that the objectql line is the same refusal. It adds the audit-specific consequence. The duplicate objectql pass is noted below and not changed here.

Measured: check:durability-log-level cannot see this site (dispatch A4)

  • The audit site calls sync.call(engine, obj.name). The checker's calleeName reads that callee as call: it unwraps .call / .apply / .bind on the log side (resolveLogCallee) but not on the guarded side. So a syncObjectSchema entry in DURABILITY_CRITICAL_CALLEES would not light this site, or the identical messaging site.
  • Adding ['syncObjectSchema', …] to the map (in a scratch copy, on 1f312fdfa) raised the seam count from 46 to 49. It turned the checker red on three sites, all outside this PR's file surface:
    • packages/cli/src/utils/schema-migration-plugins.ts:2401 (SILENT; the catch counts the failure into the plan's notes)
    • packages/metadata-protocol/src/protocol.ts:19337 (QUIET, console.warn in ensureObjectStorage)
    • packages/services/service-datasource/src/datasource-connection-service.ts:658 (QUIET, warn)
  • So, per the dispatch, the entry is not added here, and none of those sites is touched. This departs from AGENTS.md's "add it to DURABILITY_CRITICAL_CALLEES in the same PR that fixes it". The entry and its sites belong to one family card, which the seat files. Until then the unit pins below hold this site.

Pins (audit-plugin.test.ts)

  • For each of the four objects (it.each): a refused DDL logs exactly one error. The line names the object and os migrate apply and has no newline. The refusal is in meta. No could not provision line is logged at warn. The other three objects still provision, and kernel:ready resolves (the boot is not failed).
  • CONTROL: a clean provision logs zero error calls.
  • The existing keeps reporting placements when one object fails to sync now reads the line from error.
  • The wording is not pinned. The pins hold the level, the count, the named object, the named remedy command, and the refusal in meta.

Ablations (one-shot, scripts/ablation-replace.mjs in WRAP mode, run on committed 168cf1e33, restore proved by blob == HEAD and an empty git diff HEAD):

  • A, the old level back (ctx.logger.error(provisionFailureLine(…), undefined, { changed to ctx.logger.warn(provisionFailureLine(…), {): 5 failed / 10 passed. The four per-object pins fail with expected [] to have a length of 1 but got +0, and so does the placement test. The CONTROL stays green.
  • B, an error on the clean path (the line also emitted before each successful placement): 5 failed / 10 passed. The CONTROL fails with expected [ { …(3) }, { …(3) }, { …(3) }, …(1) ] to deeply equal [], and so do the four per-object pins. First attempt not run: its replacement contained its own anchor, so ablation-replace refused it (anchor count 1 to 1) and ran no test. The second anchor ran.
  • Ride-along proof: the CONTROL's pendingTables expectation was mutated to ['demo:forced-failure']. The failure message then opened with --- the serving boot's stderr (exit 0) --- and the child's full boot-diagnostics block. Restored by blob.

Tests and gates

All on head 9ac3a46f5, the PR's last commit. It changes only the changeset text after 168cf1e33.

  • pnpm --filter @objectstack/plugin-audit test: 47 files, 748 tests passed. pnpm --filter @objectstack/plugin-audit typecheck (tsc, scripts project, check:test-typecheck) passed at 168cf1e33, which has the same source.
  • @objectstack/cli, the touched integration file (--project integration, plan.telemetry-sibling.integration.test.ts): 2 passed. pnpm --filter @objectstack/cli typecheck passed, and the cli test-typecheck debt is unchanged; the touched file is not in it. The cli unit layer is left to CI: the only cli file changed is this integration test.
  • node scripts/pm/dispatch-gates.mjs --commands over the real change set derived 67 gate commands. All 67 ran, and --ran reconciles 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. Five first exited 3 (PREREQUISITE NOT MET) and were rerun green after their prerequisite was met: check-plugin-teardown-shape --self-test after fetching its pinned control commit, and check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity after a full workspace build.
  • pnpm check:durability-log-level (run explicitly; the derivation does not place it): green, 46 seams. The seam count is unchanged because this site is outside the checker's view (section above).
  • Narrowed eslint --no-inline-config over the three touched TS files: 3 files (count read from --format json), 0 errors, 0 warnings. --print-config resolves rules for each file, so none is ignored, and no parserOptions.project or projectService is set. Linting is not type-aware, so this diff cannot change the verdict on an untouched file. The repo-wide pnpm lint is left to CI.

Acceptance notes

  • objectql reports one refused object twice in one serve boot. Schema sync FAILED for object 'X' and the finished with 1 FAILED object(s) summary each print twice, in two passes about 350 ms apart (measured above). Observation only; Carrier: none.
  • The no syncObjectSchema warn in the same method still says the tables "stay lazy-created on first WRITE". That is stale for the SQL driver (measured: an insert fails with no such table). It is left as is because it guards engines without on-demand DDL, which this card does not cover. Carrier: none.
  • The resolves to NO datasource driver branch in the same loop is still a warn. No storage is created there either. It was not changed: no composition reaching it was measured, and the triage pinned only the throwing branch. Carrier: none.
  • The audit writer's per-write missing-table line says the boot log "carries Schema sync FAILED for object …". That holds whenever objectql also tried the object, which was the case in every boot measured here. It does not cover this plugin's own provisioning line, which this PR makes a sibling error with the same object name. Carrier: none.

Generated by Claude Code

…gged at error, naming what is lost and the fix

AuditPlugin's kernel:ready provisioning caught a refused DDL for
sys_audit_log / sys_activity / sys_comment / sys_comment_reaction and
logged it at warn. The boot completes and the deployment looks healthy
while the table does not exist; for the two audit tables, audited writes
keep succeeding while their rows have nowhere to land. That is the
durability class AGENTS.md "Degradation log levels" sends to error.

The catch now logs one error per object per boot. Its first line names
the object, what is lost, that the deployment keeps looking healthy, and
the measured fix: resolve the driver's refusal, then restart or run
`os migrate apply`. The driver's message goes in meta. The boot is still
not failed.

Pins: a refused DDL on each of the four objects logs exactly one error
naming it and `os migrate apply`, with the refusal in meta; the control,
a clean provision, logs no error. The telemetry-sibling integration test
now carries the serve child's own output in the assertions that read
what the boot provisioned.

Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce
Co-authored-by: Claude <[email protected]>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-audit, touching 8 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/production-readiness.mdx (via sys_audit_log (literal, a string literal in lostWithoutStorage))
  • content/docs/kernel/runtime-services/audit-service.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage))
  • content/docs/permissions/record-view-auditing.mdx (via AuditPlugin (symbol, a top-level class), sys_audit_log (literal, a string literal in lostWithoutStorage))
  • content/docs/permissions/system-context.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/plugins/packages.mdx (via AuditPlugin (symbol, a top-level class), sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_audit_log (literal, a string literal in lostWithoutStorage))
  • content/docs/protocol/kernel/index.mdx (via AuditPlugin (symbol, a top-level class))
  • content/docs/ui/setup-app.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))

⛔ 8 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/index.mdx (via sys_audit_log (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v14.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v16.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v17/17-0.mdx (via sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v17/17-1.mdx (via AuditPlugin (symbol, a top-level class), sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v17/17-5.mdx (via sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v17/17-6.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage), sys_comment (literal, a string literal in lostWithoutStorage))
  • content/docs/releases/v17/17-7.mdx (via sys_activity (literal, a string literal in lostWithoutStorage), sys_audit_log (literal, a string literal in lostWithoutStorage))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 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 a2240c4dcfc3a19f058cb0b3a4f15a351908acde → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 276071b44bbbffddcaa7d9a05677b6249006414d — the merge of head f8d993e084ff600a5a494350002351e73e2a31d6 into base a2240c4dcfc3a19f058cb0b3a4f15a351908acde, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 276071b44bbbffddcaa7d9a05677b6249006414d && git checkout 276071b44bbbffddcaa7d9a05677b6249006414d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a2240c4dcfc3a19f058cb0b3a4f15a351908acde f8d993e084ff600a5a494350002351e73e2a31d6 && git checkout -B drift-repro a2240c4dcfc3a19f058cb0b3a4f15a351908acde && git merge --no-ff f8d993e084ff600a5a494350002351e73e2a31d6

node scripts/docs-audit/affected-docs.mjs --json a2240c4dcfc3a19f058cb0b3a4f15a351908acde

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a2240c4dcfc3a19f058cb0b3a4f15a351908acde → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note · domain:services seat 1 · session_01CeRCvvvjxbxu2iYJAc5Cce · 2026-10-12T07:47Z. The seat's review is ACCEPT (6119997110 on #22913).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 08:11
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 08:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 6a40a09 Oct 12, 2026
35 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22913-audit-ddl-error-level branch October 12, 2026 08:36
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 tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-audit: a failed DDL for an audit table at boot is logged at warn and the boot stays green

2 participants