Skip to content

fix(service-package, cli): os migrate plan / apply cover sys_packages, and the boot honours OS_SKIP_SCHEMA_SYNC for it - #23060

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23022-sys-packages-managed-then-flag
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23022-sys-packages-managed-then-flag

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23022
Clause-②: no (the seat cannot read it without a build: a newly exported object definition or name flips it; the dev measures on the built declaration closure and stops on a flip)

What this does, in the card's order

  1. The documented migration path covers sys_packages. PackageServicePlugin.init() now declares the table as a kernel service, package.storage: a read-only catalog probe (SQLite sqlite_master, PostgreSQL information_schema.tables at current_schema(), MySQL information_schema.tables at DATABASE()) and the boot's own ensurePackageTable. bootSchemaStack (the funnel os migrate plan and os migrate apply share) lists the table as create_table when it is absent, and flushSchemaDdl creates it with those same statements. A migrate boot runs a provider's init() and never its start(), which is why the declaration lives in init().
  2. Then the boot honours the flag. start() reads the value ObjectQLPlugin resolved and published on the engine (skipSchemaSync, the seam plugin-audit: provisionSystemTables creates tables under OS_SKIP_SCHEMA_SYNC=1, while objectql's boot says it registered every schema WITHOUT DDL #22984 added for plugin-audit and service-messaging; no second environment read). When it is set, start() sends no DDL and no probe, logs one info line naming sys_packages, the flag and os migrate apply, and still registers the package service. Unset, the boot is unchanged.

Where the table's presence cannot be read (a default driver that names no SQL dialect, or a seam that returns no result set), the run says so in its composition notes and neither lists nor creates the table: "could not tell" is never read as "absent".

Why not a registered object (the triage's preferred shape): measured, A3 holds

A declared sys_packages, through SqlDriver.initObjects on SQLite (better-sqlite3) at this branch's base:

  • fresh database: `id` varchar(255), created_at / updated_at typed datetime with a strftime default, every other column nullable, and primary key (id) alone. A second version of one package id is refused: SQLITE_CONSTRAINT_PRIMARYKEY ... UNIQUE constraint failed: sys_packages.
  • the same object adopted over a raw-DDL table: the table is untouched and detectManagedDrift() reports 0 entries.

So fresh and existing deployments would get different tables, and the fresh one breaks multi-version publish. That is the A3 stop shape, so the managed-path entry keeps the raw DDL instead. package-table.ts already recorded the same result on PostgreSQL 16.14 and MySQL 8.0.46 (from the #21243 work). The create statements are unchanged, so apply creates exactly what the boot creates. On SQLite, sqlite_master holds byte-identical CREATE TABLE and CREATE INDEX text for both (integration reading below). PostgreSQL and MySQL were not reachable from this container: the new presence probes on those dialects are pinned by statement, not run live.

Measured through the public doors (os serve with OS_MIGRATE_AND_EXIT=1, os migrate plan --json, os migrate apply --yes --json, SQLite)

reading base 9f4b652e3 this branch 6f0d23fde
boot, flag unset, fresh sys_packages=true sys_packages=true
boot, flag set, table dropped sys_packages=true (DDL behind the flag) sys_packages=false, one NOT provisioned at boot info line
os migrate plan --json, table absent pending: [] pending: ["create_table:sys_packages"]
os migrate apply --yes --json, flag set message=in_sync, still absent created: ["create_table:sys_packages"], present
apply-created table equals boot-created table n/a VERDICT shape apply == boot: true
boot, flag unset, table dropped (CONTROL) re-created re-created

A4: Clause-②: no holds, measured on the built declarations

BASE sources of the three changed source files were built in this worktree (service-package tsup, cli tsc), then HEAD's were. The comparison covers 40 declaration files: service-package dist/index.d.ts and index.d.cts, the CLI's entry closure (index / console / hook-body, 37 files) and dist/utils/schema-migrate.d.ts. They were compared after the TypeScript printer stripped comments: VERDICT IDENTICAL: 40 identical, 0 differ. Positive control: injecting one member into declare class PackageServicePlugin and one export into schema-migrate.d.ts reads DIFFER for both. The restore was proven by blob hashes and an empty git status --porcelain, and two HEAD builds read identical. New names are module-private, and package.storage is a service name, not a declared one.

Tests

  • @objectstack/service-package: new managed-path.test.ts (18 cases). On a real node:sqlite database, the declaration creates the (id, version) table and its index, and the probe is a read. Each dialect is asked its own catalog. "Could not tell" is undefined, and no SQL is sent. With the flag set, start() sends no DDL, the table stays absent, there is one info line, and the service is registered. The CONTROL rows are unset, false and the string 'true'. package-table.test.ts's context now captures only the package service, because init() registers one more. Suite: 7 files / 113 passed; typecheck exit 0 (its tsc --listFiles includes the new test).
  • @objectstack/cli: new src/utils/schema-migrate.package-table.integration.test.ts, through the real funnel with OS_SKIP_SCHEMA_SYNC=1 set. It covers three steps. First, a plan on a never-started project lists create_table sys_packages with its seven columns and creates no file. Second, apply has nothing for the table before the flush; the flush creates it as PRIMARY KEY (id, version) with TEXT columns and idx_packages_latest (id, created_at). Third, a re-plan lists nothing for it.
  • At HEAD 9c4be9e42 (after merging main):
    • CLI --project unit: 283 files / 4270 passed.
    • CLI --project integration, the 10 files on this path: the new one, unmanaged-tables, schema-migrate.host-composition / readonly-probe / deferred-ddl / one-shot-family / plain, plan.boot-parity, plan.telemetry-sibling and apply.account-issuer-retirement. 10 files / 119 passed.
    • CLI typecheck, including check:test-typecheck: exit 0.
    • The rest of the integration project is declared to CI.

Ablations: each negative pin was put back and watched go red, then restored

Mutations went through scripts/ablation-replace.mjs, with an EXIT/INT/TERM trap that restores from HEAD. Each restore was proven by blob hash and an empty git diff HEAD.

  • The boot creates nothing under the flag. Mutation: if (false && bootSchemaSyncSkipped(objectql)). Result: managed-path.test.ts 2 failed | 16 passed, expected [ …(2) ] to deeply equal [] (the two DDL statements were sent) and expected true to be false (the table exists).
  • The migrate boot does not create the table before the flush. Mutation: the CLI calls ensure() while planning. Result: 1 failed, expected [] to deeply equal [ { table: 'sys_packages', …(2) } ].
  • The producer, across the package boundary. Mutation: PACKAGE_STORAGE_SERVICE renamed to 'package.storage.ablated' in service-package, which was then rebuilt. ablation-dist-preflight reported "marker present in 2 built files". Result: the CLI test failed 1, expected [] to deeply equal [ { table: 'sys_packages', …(2) } ]. Restore leg: rebuilt, preflight --absent reported "marker absent from all 6 built files", and the test passed 1.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9c4be9e42 derived 68 families over 7 changed paths, with merge base df7a13792. All 68 ran, and all exited 0. check:dual-build-cjs-loads and check:i18n-coverage first answered PREREQUISITE NOT MET (exit 3: packages outside the CLI closure were not built). They were re-run green on the same HEAD after a full turbo build. --ran reports: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED.

Lint was narrowed and the narrowing proven. eslint --no-inline-config --format json over the 6 changed .ts files reports 6 files, 0 errors, 0 warnings, ESLint exit 0. The config lints all 6 files (no ignore warning) and enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. The changeset .md is outside eslint's population.

Acceptance notes (not filed)

  • The seam is a string-named kernel service, package.storage. Like skipSchemaSync, it is an undeclared surface, chosen so that no name is added to either package's declarations. It is held together by the CLI integration test booting the real plugin, and the producer ablation shows that test failing when the name moves. A declared accessor would be the contract-first form and widen the published surface. Carrier: none.
  • The flag-half pins in service-package use an engine double that carries skipSchemaSync: true. service-package does not depend on @objectstack/objectql, and the unaliased-test-import ledger is shrink-only. The real producer was exercised once, through os serve (table above). Its publication is pinned by plugin-audit's and service-messaging's real-producer seam tests. Carrier: none.
  • On a flag deployment that boots before running os migrate apply, hydration's list() meets the missing table. The SQL driver logs its own DATABASE_ERROR — no such table: sys_packages line, and hydration logs at debug. That is loud enough to find, and the changeset says to run os migrate apply first. Carrier: none.
  • The presence probe asks for the table, not for idx_packages_latest. A table that exists without its index (reachable only by hand) is not re-planned. Carrier: none.
  • os migrate plan's unmanaged-table sweep still lists sys_packages when the table exists. Its wording, "declared by no object in this plan", stays literally true: the table is declared by a service, not by an object. The boot-parity pins still expect it. Carrier: none.
  • os migrate plan on a fresh project now lists one more create_table. That is intended: it is what apply will create.
  • Merged main at 9c4be9e42 before opening. The incoming commits touch packages/spec, the security package-verb walls and CI, and none of this diff. durability log level: schema-sync DDL refusals in four paths log below error, and check:durability-log-level cannot see them (no syncObjectSchema entry; sync.call reads as callee "call") #22985's region of schema-migration-plugins.ts is untouched: this change lives in schema-migrate.ts.

Generated by Claude Code

… the boot honours OS_SKIP_SCHEMA_SYNC

PackageServicePlugin.init() declares sys_packages as the package.storage
service (presence probe + the boot's own ensurePackageTable); bootSchemaStack
lists it as create_table when absent and creates it on flush, so os migrate
plan / apply cover it. start() then skips its DDL when the engine resolved
skipSchemaSync, logging one info line.

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

Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce
Co-authored-by: Claude <noreply@anthropic.com>
…hrough the real funnel

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

Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/service-package, touching 17 documentable anchor(s).

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e49aaec733d5cebd82a6b2e5521cdaae998b5b8d.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 38 pages)
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 28 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 e49aaec733d5cebd82a6b2e5521cdaae998b5b8d → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 e49aaec733d5cebd82a6b2e5521cdaae998b5b8d → 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 14:50
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 14:50
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 84c4fff Oct 12, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23022-sys-packages-managed-then-flag branch October 12, 2026 15:12
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/l tests tooling

Projects

None yet

2 participants