Skip to content

fix(cli): os migrate plan no longer lists sys_packages as unmanaged after os migrate apply created it - #24226

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24059-unmanaged-sweep-sys-packages
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24059-unmanaged-sweep-sys-packages

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24059

Clause-②: no

What changes

os migrate plan no longer lists sys_packages under "declared by no object in this plan" after os migrate apply has created it. A genuinely stranded sys_* table is still listed.

sys_packages is the package registry's table. It is not an object. PackageServicePlugin.start() creates it with raw DDL, and since #23022 os migrate apply creates it from the registry's package.storage declaration. The plan's unmanaged-table sweep built its known set from the planned drivers' managed tables and the declared objects only. sys_packages is neither, so every plan after an apply reported the live registry table as if it were stranded.

  • packages/cli/src/utils/schema-migrate.ts: findPackageStorage(kernel) is now the one reader of the package.storage declaration (the lookup and the shape check that used to open planPackageTable). bootSchemaStack reads it once per boot. planPackageTable plans the table from that object as before, on a deferring boot only. The same object's table is exposed as SchemaStack.nonObjectTables. It is filled on every boot, whether or not the table exists and whether or not anything is planned for it, and it is [] when the boot composed no package registry.
  • packages/cli/src/utils/unmanaged-tables.ts: buildKnownTableNames(managed, declaredObjects, nonObjectTables) adds those names verbatim. collectUnmanagedTables takes a required nonObjectTables option and passes it through. The module header's list of legitimate platform-prefixed families gains this one as item 4. No table name is spelled in the sweep.
  • packages/cli/src/commands/migrate/plan.ts: passes stack.nonObjectTables to the sweep. The sweep reads the declaration from the same boot and kernel the plan runs in.

Measured before changing anything (PM hypotheses)

  • H1, confirmed at aa31569b2: buildKnownTableNames returned the managed set plus each declared object's name and resolved table name, and selectUnmanagedTables reported every platform-prefixed table outside it.
  • H2, confirmed: planPackageTable returns null when the table is present. Door reading below: after the apply, the plan's pending is [], so the planned work names nothing. The fix therefore reads the declaration, not the planned work. The sweep runs in the same boot as the plan: bootSchemaStack reads the declaration from the kernel it booted, and plan.ts hands it to the sweep from the returned stack.
  • H3, pinned as a decision: when no package registry is composed, nonObjectTables is [], and a sys_packages table left in that database is still reported. Through the door this case does not arise on a mirrored boot. The sweep runs only when the composition mirrors the served boot, and that composition always mounts PackageServicePlugin through the always-on package-registry slate (composeServedPlatform in schema-migration-plugins.ts, and the base reading's composition note lists it). So the decision is pinned at unit level ("DECISION: a boot that composed no package registry …").
  • H4, re-measured at base aa31569b2 on examples/app-crm. Setup: a fresh sqlite file with sys_legacy_widget created up front, fresh OS_HOME, DB env unset, run through packages/cli/bin/run-dev.js with --database-url file:DB. Then migrate apply --yes, then migrate plan, then migrate plan --json:
    • base: apply printed + sys_packages [create_table, 7 column(s)] and Created/extended 80 table(s). The plan printed 2 table(s) … declared by no object in this plan: with • sys_legacy_widget and • sys_packages. --json: unmanagedTables.tables = sys_legacy_widget, sys_packages, physicalTables 81, pending [].
    • head 04c0d95ec: same apply. The plan printed 1 table(s) …: with • sys_legacy_widget only. --json: tables = sys_legacy_widget, physicalTables 81, pending []. Both tables are physically present after each run.

Pins

  • unmanaged-tables.test.ts, new describe [#24059] a table the composition declares outside the object set. It has three cases: the known set holds the name only when it is declared; after an apply the registry table is not reported and sys_legacy_widget still is; and the H3 decision. Every existing buildKnownTableNames / collectUnmanagedTables call states nonObjectTables explicitly.
  • unmanaged-tables.integration.test.ts uses a real boot with the composition plan uses. sys_packages joins the negative controls, and the premise case asserts that stack.nonObjectTables equals the table of the kernel's own package.storage service while nothing is pending for it.
  • schema-migrate.package-table.integration.test.ts: nonObjectTables is ['sys_packages'] on the plan of a never-started project and on the plan after the apply, when pending holds nothing for the table.
  • Two door-level pins encoded the defect, and they flip with this change. plan.boot-parity.integration.test.ts expected the sweep to name exactly sys_packages, and now expects it to name nothing. plan.telemetry-sibling.integration.test.ts expected sys_packages plus the sibling's retired table, and now expects only the retired table. Both pins were green at base with the old expectation, so the new one fails against the old sweep. The H4 base reading shows the same thing.

Ablation (fix committed first, at 04c0d95ec)

node scripts/ablation-replace.mjs --file packages/cli/src/utils/unmanaged-tables.ts --anchor 'for (const table of nonObjectTables) known.add(table);' --delete -- pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/utils/unmanaged-tables.test.ts, under the verify lock:

  • the anchor went 1 to 0, and the blob went 99e653e8d5d0 to 33c383315c85;
  • Tests 2 failed | 43 passed (45). The two red cases are the "known set" case and the "after an apply" case. The DECISION case stayed green, as expected, because it asserts the direction without a declaration;
  • restore: the blob equals HEAD (99e653e8d5d0), git diff HEAD is empty, and git status --porcelain is empty.

The unit suite imports ./unmanaged-tables.js relatively, so it reads src/ and no dist/ rebuild applies.

Tests and gates

All readings are at head 7a3803bd3 unless another commit is named. 04c0d95ec is the fix commit. 7a3803bd3 merges origin/main b9ac35cbf, which touches CI workflows and only one file in packages/cli: test/published-entry-node-env-source-reroute.test.ts.

  • pnpm --filter @objectstack/cli typecheck at 04c0d95ec: exit 0. That is tsc --noEmit, then check:test-typecheck, which held 3 files, 28 errors and 6 signatures. tsc -p packages/cli/tsconfig.json --listFilesOnly puts all 5 touched test files in its 1611-file program.
  • Unit tier at 04c0d95ec, pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 2 failed | 308 passed (310), Tests 4573 passed | 29 skipped. The two files, test/published-subpath-console.pin.test.ts and test/published-subpath-hook-body.pin.test.ts, were refused before their bodies ran ("packages/cli is not built"). After pnpm --filter @objectstack/cli build, both pass: Tests 29 passed (29).
  • Integration tier at 04c0d95ec, the four touched files with --project integration: unmanaged-tables.integration and schema-migrate.package-table.integration 4 passed, plan.boot-parity.integration 3 passed, plan.telemetry-sibling.integration 2 passed.
  • After the merge: test/published-entry-node-env-source-reroute.test.ts and src/utils/unmanaged-tables.test.ts, 50 passed.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 7a3803bd3: 64 commands, merge base b9ac35cbf, 9 paths, 232 changed lines. All 64 ran, with each exit code captured before any pipe. 62 exited 0 on the first run. check:dual-build-cjs-loads and check:i18n-coverage exited 3 (PREREQUISITE NOT MET); both exited 0 after turbo run build --filter='!@objectstack/docs' --concurrency=2 (76 tasks, 75 cached). --ran: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.
  • pnpm exec eslint --no-inline-config over the 8 changed .ts files: exit 0. Repo-wide pnpm lint is left to CI.

Acceptance notes

  • One more refusal path, stated. findPackageStorage now runs on every boot, not only a deferring one. A package.storage service of the wrong shape (a version skew between @objectstack/cli and @objectstack/service-package) now also fails the skip-schema-sync boot of plan, apply and organization-ownership (the Turso remote transport). It fails with the release-mismatch error those commands already gave on every deferring boot. Only those three commands, plus unmapped-columns, compose the registry. The changeset says so.
  • The section's sentence still reads "declared by no object in this plan". The rows it lists are still declared by nothing, so the wording is unchanged.
  • nonObjectTables is a union across every planned database, like the managed set beside it. The registry writes to the default driver, so a sys_packages table in the telemetry sibling would also go unreported. Over-excluding is the module's stated safe direction. Not measured; no carrier.

Generated by Claude Code

…eclares outside the object set

os migrate plan listed sys_packages under "declared by no object in this
plan" right after os migrate apply created it. The sweep's known set was the
planned drivers' managed tables plus the declared objects; sys_packages is
neither. bootSchemaStack now reads the package.storage declaration once
(findPackageStorage), plans the table from it as before, and exposes its
table name on every boot as SchemaStack.nonObjectTables, which plan.ts hands
to collectUnmanagedTables and buildKnownTableNames adds verbatim.

Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 9 documentable anchor(s).

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

  • content/docs/ai/skills-reference.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/data-modeling/indexing.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/cli.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/index.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/kernel/services-checklist.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/upgrading.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

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

  • content/docs/releases/v17/17-0.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-4.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-5.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-6.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-7.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

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
  • 1 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 — 29 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 b9ac35cbfe60afcea88947ed4eaf8c50e3efc214 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 b9ac35cbfe60afcea88947ed4eaf8c50e3efc214 → 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

席位核对 — ACCEPT · PR #24226 at 7a3803bd3 (#24059)

domain:cli seat 2 · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-17T22:50Z · holder of claim 6241536444. The dev's report is comment 6242543964 on #24059, and the seat reviewed it against this PR, not the report's own account.

  1. Shape. Draft → main. The first line is Fixes #24059, the only closing keyword. The body's other card number (service-package: sys_packages is created by raw boot DDL that ignores OS_SKIP_SCHEMA_SYNC=1, and os migrate apply (the flag's documented path) does not create it #23022) is closed and has no verb beside it. Clause-②: no is line-initial, and the assignee is marchtian. 9 files, inside the claimed surface:

    • unmanaged-tables.ts;
    • schema-migrate.ts, which exposes the declaration it already read;
    • plan.ts, a 3-line pass-through;
    • five pins;
    • one changeset.

    check-governed-merges.mjs --pr 24226: NOT governed, 232 changed lines.

  2. Premise and fix, read on the diff.

    • findPackageStorage(kernel) is now the one reader of the package.storage declaration. planPackageTable takes its result instead of re-reading the service.
    • SchemaStack.nonObjectTables is [packageStorage.table] on every boot, or [] when no registry is composed.
    • buildKnownTableNames adds those names verbatim, and no table name is spelled in the sweep (the card's second "Done when" line).
    • H2 is confirmed in code: planPackageTable returns null once the table is present. So the fix rightly reads the declaration, not the pending work.
    • The sweep has one production caller (plan.ts:229), and it passes stack.nonObjectTables.
  3. Door reading, from the PR. examples/app-crm with apply --yes then plan:

    • base aa31569b2 lists sys_legacy_widget and sys_packages;
    • head 04c0d95ec lists sys_legacy_widget only;
    • physicalTables is 81 both times, and pending is [] both times.

    The ablation of the one added line reddens 2 unit cases, and the DECISION case stays green as designed. The restore is proven by blob. The two door pins that encoded the defect (plan.boot-parity, plan.telemetry-sibling) flip to the corrected expectation, and NOT_AN_OBJECT is still read where it guards the raw-DDL table's existence.

  4. Clause-②: no, read. The diff touches no published schema, export or HTTP accept set. One refusal now also reaches a skip-schema-sync boot: a wrong-shape package.storage (a CLI/service-package release skew), with the existing release-mismatch error. That error is not a public accept set. The changeset states it. No contract-review face is touched.

  5. Check-runs on head 7a3803bd3 at this act: 29 success, 6 skipped, 4 in_progress. Landing waits on every check being green or a roster skip. Local: 64 derived, 64 run (--ran exit 0); ESLint exit 0 over the 8 changed .ts files.

  6. Accepted deviations:

    • one merge of main (b9ac35cbf) to clear a stale-tree warning, followed by a scoped re-run;
    • the two door pins flipped, which is the defect's own encoding;
    • three stack doubles left without nonObjectTables, because the sweep is mocked or not mirrored there (68 tests passed);
    • one voided gate launch (a relative path), relaunched and the second launch cited.

    Changeset, read sentence by sentence:

    • "Now: the sweep also counts the tables the boot's composition declares outside the object set … reads it on every boot" matches bootSchemaStack.
    • "Still reported: a sys_packages … that composes no package registry" matches [] plus the DECISION pin.
    • "Refused in one more case" matches findPackageStorage running unconditionally.
    • "Unchanged: the --json keys, the section's wording, and what apply creates" is true: UnmanagedTablesReport is unchanged, and planPackageTable still runs only on deferring boots.
  7. Out of scope: nonObjectTables is a union across planned databases, so a sys_packages in the telemetry sibling would also go unreported. That is the module's stated over-exclude direction. Noted, not filed: no reach was measured. By the dev's reading the registry writes only to the default driver.

  8. Landing. Ordinary queue landing once every check on this head is green or a roster skip. Fixes #24059 closes the card, and the seat strips pm:dispatched in the same act.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 23:04
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 23:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit d3d0644 Oct 17, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24059-unmanaged-sweep-sys-packages branch October 17, 2026 23:28
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

2 participants