Skip to content

fix(plugin-audit, service-messaging): boot-time system-table provisioning honours OS_SKIP_SCHEMA_SYNC - #23021

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22984-boot-provision-honours-skip-sync
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22984-boot-provision-honours-skip-sync

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22984

Clause-②: no (the seat cannot read it without a build: any new name on a package entry's declared surface, an accessor included, flips it; the dev measures on the built declaration closure and stops on a flip)

What this changes

OS_SKIP_SCHEMA_SYNC=1, or ObjectQLPlugin's skipSchemaSync option, declares that a deployment's DDL is managed out-of-band. The engine's boot honoured it. AuditPlugin.provisionSystemTables and MessagingServicePlugin.provisionSystemTables did not, so a flag deployment got its system tables re-created at kernel:ready while the boot log said no DDL ran. The direction is the triage grade (comment 6121183235): the fix is in the provisioning plugins, and syncObjectSchema is not gated.

  • packages/objectql/src/plugin.ts (the producer). init() publishes the skipSchemaSync value the plugin resolved (the option, else OS_SKIP_SCHEMA_SYNC === '1') on the engine instance it registers. It is the undeclared own property skipSchemaSync, keyed by the module constant SKIP_SCHEMA_SYNC_ENGINE_KEY, which is not exported. The option's TSDoc gains one paragraph saying the value also governs the plugins' boot provisioning.
  • packages/plugins/plugin-audit/src/audit-plugin.ts. provisionSystemTables hoists its four objects into objects, then checks bootSchemaSyncSkipped(engine) first. That is a module-private probe, engine.skipSchemaSync === true. When it is set, the method logs one info line (provisionSkippedLine) and returns before any DDL or probe. The rest of the method is unchanged. One docblock sentence became false with this change and is corrected: "OS_SKIP_SCHEMA_SYNC is NOT named: this method issues its DDL whether or not that flag is set". A new "Why the flag skips this provisioning" section is added.
  • packages/services/service-messaging/src/messaging-service-plugin.ts. Same shape. The objects array now sits above the syncObjectSchema probe. Next comes the bootSchemaSyncSkipped(engine) early return with its one info line, then the unchanged const sync = engine.syncObjectSchema; if (typeof sync !== 'function') return; and the per-object loop.
  • .changeset/22984-boot-provision-honours-skip-sync.md: patch for all three packages.

#22985 (queued, same messaging function) remains open. The shape it will meet is the one in the messaging bullet above.

Measurements (the dispatch's assumptions)

A1, confirmed on origin/main 8e4a2302a. Neither provisionSystemTables read the flag: the flag's only mention in either file was the audit docblock sentence above. ObjectQLPlugin held the resolved value in private skipSchemaSync and exposed no accessor.

A2, confirmed for all twelve tables, so the skip is built in both plugins. Measured through os itself (packages/cli/bin/run-dev.js, BASE tree 8e4a2302a). os serve with OS_MIGRATE_AND_EXIT=1 provisions a fixture app with requires: ['auth']. Every physical table backing the twelve objects is dropped. Then os migrate plan --json and os migrate apply --yes run.

shape migrate plan lists create_table migrate apply creates
one database (OS_TELEMETRY_DB=0) 12 / 12 12 / 12 (Created/extended 12 table(s))
telemetry sibling (sys_audit_log, sys_activity, sys_notification, sys_notification_delivery and sys_http_delivery land in boot.telemetry.db) 12 / 12 12 / 12, each in its own store
OS_SKIP_SCHEMA_SYNC=1 set in the plan and apply environment 12 / 12 12 / 12

The tables: sys_audit_log, sys_activity (physically sys_activity__r20261012), sys_comment, sys_comment_reaction, sys_notification, sys_inbox_message, sys_notification_receipt, sys_notification_delivery, sys_notification_preference, sys_notification_subscription, sys_notification_template and sys_http_delivery.

A3: the seam adds no entry-reachable name, so Clause-②: no holds. The seam is the optional probe of the concrete engine: an undeclared instance property, with no public method, exported type or kernel-service contract. It was measured on the built declarations. The BASE dist declaration files (.d.ts, .d.mts, .d.cts) of all three packages were snapshotted before editing and rebuilt after. Each pair was compared raw, and again after the TypeScript printer with removeComments: true.

  • Across all 10 declaration files, the comment-stripped declarations are IDENTICAL. The raw bytes differ only in TSDoc: the new option paragraph in objectql's index.d.ts and index.d.mts, and the private method's docblock in plugin-audit and service-messaging. That raw difference also proves each .d.ts was rebuilt from this source.
  • POSITIVE CONTROL: the rebuilt util-Ch_WDmDU.d.ts, which holds declare class ObjectQL, reads as DIFFER at that line once one readonly skipSchemaSync: boolean; member is injected. A first control attempt anchored on index.d.ts matched nothing (0 anchor hits) and was discarded as void.

A4: the skip line is info. Under the flag nothing is degraded. The operator declared DDL out-of-band, and nothing claims persistence it does not have, so the AGENTS.md "Degradation log levels" question answers No. If a table is in fact missing, the writers report it on first use (audit-writers.ts logs the missing-table write at error). The line names every skipped object, the flag and os migrate apply.

A5, held. Under the flag provisionSystemTables makes no engine call in either plugin. The audit unit pin asserts the recording engine saw zero calls: no syncObjectSchema, getDriverForObject, getDefaultDriverName or find.

Integration-level reading of the triage pin

os serve with OS_MIGRATE_AND_EXIT=1, OS_TELEMETRY_DB=0, OS_LOG_LEVEL=info, and the A2 fixture:

  1. Boot 1 with the flag unset.
  2. Drop sys_comment_reaction (audit) and sys_inbox_message (messaging).
  3. Boot 2 with OS_SKIP_SCHEMA_SYNC=1.
  4. Boot 3 with the flag unset, as the CONTROL.

BASE 8e4a2302a shows the defect in both plugins. The card left the messaging half unmeasured.

after boot 2 (flag set): sys_comment_reaction=true sys_inbox_message=true
  INFO Skipping schema sync (OS_SKIP_SCHEMA_SYNC=1) — assuming DDL is managed out-of-band; registered 75 object schema(s) with their drivers WITHOUT DDL
  INFO AuditPlugin: system tables provisioned — sys_audit_log→com.objectstack.driver.sql, sys_activity→com.objectstack.driver.sql, ...

This branch:

after boot 2 (flag set): sys_comment_reaction=false sys_inbox_message=false
  INFO [messaging] sys_notification, sys_inbox_message, sys_notification_receipt, sys_notification_delivery, sys_notification_preference, sys_notification_subscription, sys_notification_template, sys_http_delivery NOT provisioned at boot — the boot's schema sync is skipped (`skipSchemaSync` / OS_SKIP_SCHEMA_SYNC=1), declaring this deployment's DDL managed out-of-band, so this service issues none either. `os migrate apply` creates any that are missing (`os migrate plan` lists them as `create_table`).
  INFO AuditPlugin: sys_audit_log, sys_activity, sys_comment, sys_comment_reaction NOT provisioned at boot — the boot's schema sync is skipped ...
after boot 3 (flag unset): sys_comment_reaction=true sys_inbox_message=true

Tests

New pins in each plugin's test file (audit-plugin.test.ts, messaging-service-plugin.test.ts):

  • Flag set. No DDL is issued, and the audit pin sees no engine call at all. The lazy-table model's table stays absent. Exactly one info skip line names every object, the flag and os migrate apply. Nothing about provisioning is logged at warn or error.
  • CONTROL. With the flag published as false, every object is provisioned exactly as before.
  • Seam table, with the REAL ObjectQLPlugin as producer (registerProtocol: false, the engine's syncObjectSchema spied). Option true skips. Option unset with OS_SKIP_SCHEMA_SYNC=1 skips. Both unset provisions. Option false with OS_SKIP_SCHEMA_SYNC=1 provisions; this is the row that discriminates, because a plugin that read the environment itself would skip there.

Ablations. Each used scripts/ablation-replace.mjs with an EXIT/INT/TERM trap, and each restore was proven by blob hash equal to HEAD and an empty git diff HEAD.

  • Consumer, audit. Early return disabled (if (false && bootSchemaSyncSkipped(engine))): 3 red, 18 green. Red were the flag-set pin (expected [ 'getDefaultDriverName', …(8) ] to deeply equal []) and the two skip rows of the seam table (expected "syncObjectSchema" to not be called at all, but actually been called 4 times).
  • Consumer, messaging. Same mutation: 3 red, 12 green (… called 8 times).
  • Producer, objectql. The publish was replaced by void SKIP_SCHEMA_SYNC_ENGINE_KEY; and objectql rebuilt (14/14). ablation-dist-preflight.mjs --absent proved the publish gone from all 14 built files. Result: audit 2 red, 19 green; messaging 2 red, 13 green. In each suite the red rows were the two skip rows of the seam table. The restore leg rebuilt, the preflight found the publish back in dist/index.js and dist/index.mjs, and the tree was clean.
  • A discarded first producer run. It deleted the statement outright, and its build exited 1 (13/14: the now-unused constant). That run was discarded even though its direction matched.

Suites at 8afbc3b90:

  • @objectstack/plugin-audit: 47 files, 761 passed. Typecheck exit 0; the test layer goes through tsconfig.test.json, and audit-plugin.test.ts is in that program.
  • @objectstack/service-messaging: 48 files, 540 passed. Typecheck exit 0; --listFiles includes messaging-service-plugin.test.ts.
  • @objectstack/objectql (vitest run --project local): 406 files, 7936 passed. Typecheck exit 0.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 68 commands over the 6 changed paths (merge base 8e4a2302a). All 68 ran at 8afbc3b90 and every one exited 0. The --ran verdict reads 68 run and 0 NOT-MEASURED, derived from the recorded exit codes. Three gates needed a prerequisite first and were re-run on the same HEAD:

  • check-engine-split-ratio --days 90 refused on the shallow clone; history was deepened to 2026-07-07.
  • check:dual-build-cjs-loads and check:i18n needed built output.

Lint, narrowed and proven. eslint --no-inline-config --format json over the 5 changed .ts files reported 5 files, 0 errors and 0 warnings. eslint's own config covers all 5 (no ignore notice) and excludes the changeset (File ignored because no matching configuration was supplied.). The repo's config enables no type-aware linting, so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint run is CI's.

Acceptance notes

  • Same family, NOT fixed here (reported to the seat). PackageServicePlugin.start() creates sys_packages with a raw CREATE TABLE IF NOT EXISTS under the flag too: measured dropped, then re-created by a flag boot. os migrate apply does not create it: plan does not list it, and after apply it stays absent. Honouring the flag there would leave a flag deployment without sys_packages. That is the decision A2 reserves for the maintainer.
  • The objectql boot prints its Skipping schema sync (OS_SKIP_SCHEMA_SYNC=1) … info line twice per flag boot. This is cosmetic and not filed.
  • The branch was not merged with origin/main before opening. Six commits (63 files) landed since 8e4a2302a, none of them in this diff or in serve / migrate. CI runs the merge ref.

Generated by Claude Code

…ning honours skipSchemaSync

ObjectQLPlugin publishes the skipSchemaSync value it resolved (the option,
else OS_SKIP_SCHEMA_SYNC=1) on the engine instance as an undeclared
property. AuditPlugin and MessagingServicePlugin read it in
provisionSystemTables and, when it is set, issue no DDL and log one info
line naming the objects they did not provision and the os migrate apply
remedy. With the flag unset the provisioning is unchanged.
syncObjectSchema itself stays ungated: a runtime publish still gets its
table.

Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce
Co-authored-by: Claude <noreply@anthropic.com>
…ovisioning skip

Per plugin: with the flag published on the engine, no DDL and no probe is
issued, the table stays absent, and one info line names the skip; the
CONTROL provisions exactly as before. A seam table boots the real
ObjectQLPlugin as producer: option true and the env fallback skip, unset
provisions, and an explicit skipSchemaSync false beats
OS_SKIP_SCHEMA_SYNC=1. Also turns two docblock links to a module-private
function into plain code spans.

Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/plugin-audit, @objectstack/service-messaging, touching 9 documentable anchor(s).

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

  • content/docs/automation/webhooks.mdx (via MessagingServicePlugin (symbol, a top-level class))
  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/record-view-auditing.mdx (via AuditPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via AuditPlugin (symbol, a top-level class), ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via AuditPlugin (symbol, a top-level class), ObjectQLPlugin (symbol, a top-level class))

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

  • content/docs/releases/v14.mdx (via MessagingServicePlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via AuditPlugin (symbol, a top-level class), ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via MessagingServicePlugin (symbol, a top-level class))

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 — 24 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 9432bac077ecbfa703c09409f3fed416edd41d99 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 783471949ce4dbf657e65ab5da2806516d23e119 — the merge of head de65696a4938d59e0afe092c62d1347c6ae87114 into base 9432bac077ecbfa703c09409f3fed416edd41d99, 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 783471949ce4dbf657e65ab5da2806516d23e119 && git checkout 783471949ce4dbf657e65ab5da2806516d23e119
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9432bac077ecbfa703c09409f3fed416edd41d99 de65696a4938d59e0afe092c62d1347c6ae87114 && git checkout -B drift-repro 9432bac077ecbfa703c09409f3fed416edd41d99 && git merge --no-ff de65696a4938d59e0afe092c62d1347c6ae87114

node scripts/docs-audit/affected-docs.mjs --json 9432bac077ecbfa703c09409f3fed416edd41d99

⚠️ 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 9432bac077ecbfa703c09409f3fed416edd41d99 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 11:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 11:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit d294951 Oct 12, 2026
35 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22984-boot-provision-honours-skip-sync branch October 12, 2026 11:29
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-audit: provisionSystemTables creates tables under OS_SKIP_SCHEMA_SYNC=1, while objectql's boot says it registered every schema WITHOUT DDL

2 participants