Skip to content

fix(cli): os migrate plan's unmanaged-tables sweep gates on whether the composition mirrors the served boot, sweeps every planned database, and states only a reason that holds (#22580) - #22732

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22580-unmanaged-tables-reason
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22580-unmanaged-tables-reason

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22580
Clause-②: no

What changed

os migrate plan's unmanaged-tables sweep reports platform-prefixed tables that no object declares. It used to decide whether it could run by reading composition.hostConfigLoaded. That flag is false on a project with a compiled artifact and no host config too. Since PR #22574, that project's composition is what os serve registers, so the sweep was withheld there with a reason that no longer held.

  • One fact, recorded where it is decided. SchemaMigrationComposition gains servedBoot, which is either { mirrored: true } or { mirrored: false, reason }. The reason is one of not-composed, nothing-to-compose, config-unloadable or stack-only. buildSchemaMigrationPlugins sets it at the two places that decide it: the catch that finds the host config unloadable, and the branch that composes what serve mounts around the stack (PR fix(cli,driver-sql): os migrate plan/apply compose what os serve mounts around the stack, so sys_account.issuer is a drop (#22506) #22574's composeServedPlatform). NOTHING_COMPOSED and bootSchemaStack's non-composing literal carry their own values.
  • The gate reads that fact, and nothing else. collectUnmanagedTables runs exactly when servedBoot.mirrored is true, and it never infers from hostConfigLoaded. When the fact says no, the detail is the sentence for the recorded reason, one sentence per reason. A Record keyed by the reason union means a new reason does not compile until it has its own sentence.
  • Every database the plan covers. The sweep now reads the catalog of every driver the plan diffs (stack.drivers), so the telemetry sibling is included. The known set is the union of every planned driver's managed set and the declared names. If any one database cannot be read, the whole report is unreadable, and the detail names that database.

The --json shape of unmanagedTables is unchanged. physicalTables now counts every database swept, and the human line says "in the database(s) this plan covers". The changeset is .changeset/22580-unmanaged-tables-served-boot.md (@objectstack/cli patch).

Measured through the public door (os migrate plan --json, built CLI)

Fixtures and script: an artifact project declaring requires: ['auth'], provisioned first by os serve's own provision-and-exit (OS_MIGRATE_AND_EXIT=1), with a sys_-prefixed orphan table planted in it.

shape main e5899a6 this branch d7b741e
compiled artifact, no config (production, OS_TELEMETRY_DB=0): 75 objects composed, 75 of 75 examined unreadable: "no host config, so the composed object set is the compiled artifact plus the platform floor". False. read, physicalTables: 77, tables sys_os22580_orphan, sys_packages
neither config nor artifact: 4 managed tables, the data stack unreadable with the same sentence. False: there is no artifact and no platform floor. unreadable: "neither a host config nor a compiled artifact, so this plan's object set is the data stack alone". True.
host config that throws (exit 1) unreadable, names the config. True. unchanged
development boot with a telemetry sibling, orphans planted in both databases config project: read, physicalTables: 70, primary orphan only. The sibling's sys_os22580_sibling_orphan was never looked at. artifact project: read, physicalTables: 78, both orphans plus sys_packages

PM mechanism assumption 4 (the sibling) was measured as a false statement: a read answer was given beside a telemetryDatabase the sweep never read. Per the dispatch, that makes it in scope.

Tests (all on d7b741efe)

  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 280 files, 4169 tests passed.
  • pnpm --filter @objectstack/cli typecheck: exit 0. That is tsc --noEmit, plus check:test-typecheck "OK — 3 file(s) / 28 error(s) / 6 pinned signature(s) held". Counted with --listFiles: every touched test file is in exactly one program. The five under src/ are in tsconfig.json; the test/ e2e file is in tsconfig.test.json.
  • Integration tier, run locally because this diff edits these files:
    • src/utils/unmanaged-tables.integration.test.ts: 3 passed. Its boot now also passes composeServedPlatform, as plan does, and asserts servedBoot.
    • src/commands/migrate/plan.boot-parity.integration.test.ts: 3 passed. The card's pin: the sweep must read on every shape, the compiled-artifact project included, and name exactly sys_packages. Before, this check was conditional on the sweep having run.
    • src/commands/migrate/plan.telemetry-sibling.integration.test.ts: 2 passed. A retired-table orphan planted in the sibling is reported.
  • OS_TEST_TIERS=nightly, test/migrate-unloadable-host-config-exit.e2e.test.ts: 12 passed. The card's control: unloadable config gives unreadable naming the config; no config and no artifact gives unreadable naming that absence; loadable config gives read. This file is in the nightly tier, so per-PR CI does not run it. Per PR, the unit tests carry the same control.
  • New unit pins:
    • servedBoot per project shape, in schema-migration-plugins.test.ts. The artifact case asserts hostConfigLoaded: false beside mirrored: true.
    • In unmanaged-tables.test.ts: every reason, with distinct details and named subjects; the artifact-shaped composition sweeps; the multi-database cases.
  • Ablations through node scripts/ablation-replace.mjs, each restored with "blob == HEAD (79269b2509df) and git diff HEAD is empty":
    • A1 put the gate back to "no host config means unreadable". unmanaged-tables.test.ts went to 2 failed / 39 passed: the artifact sweep, and the reason that holds. A first A1 attempt was a no-op: the tool refused it because the replacement contained the anchor ("anchor count moved 1 -> 1"), and nothing ran. It was re-run with a non-overlapping anchor.
    • A2 limited the catalog read to the first driver. 3 failed / 38 passed: all three multi-database cases.

Gates (all on d7b741efe)

  • Every line of the dispatch's 50-line gate list ran, and node scripts/pm/dispatch-gates.mjs --commands re-derived 15 more on the real change. Reconciliation with --ran: "65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN".
  • Two gates, check:dual-build-cjs-loads and check:i18n-coverage, first answered exit 3 PREREQUISITE NOT MET because unrelated packages had no dist/. After turbo run build --filter=!@objectstack/docs, both are green:
    • "107 published require entry point(s) across 66 package(s) load"
    • "OK (13 config(s), 621 baselined untranslated string(s), none new)"
  • pnpm lint (the full eslint . --no-inline-config): exit 0 in 120 s. As a narrowed cross-check, the 10 touched .ts files gave 10 results in the --format json output, 0 errors, 0 warnings, none ignored. This repo's eslint.config.mjs enables no type-aware linting, so this diff cannot move any untouched file's verdict.
  • Public face: none of the changed symbols is on @objectstack/cli's published entry points. dist/index.d.ts, console.d.ts and hook-body.d.ts each return 0 hits. As a positive control, dist/utils/unmanaged-tables.d.ts does hit. No export is added there.

Deviations, stated

Acceptance notes

  • Findings carry no per-database attribution, because the --json shape is kept as the claim required. In a two-database plan, a finding names the table, and the human line says the databases the plan covers.
  • servedBoot is not added to the --json composition block. Consumers keep reading hostConfigLoaded, which the earlier unloadable-config ruling pinned.
  • stack-only and not-composed cannot be reached through os migrate plan, its only caller, because that caller asks for both compositions. They exist so the fact is true for every composition: security-catalog-overlays, and boots that do not compose.
  • The known set is a union by name, which errs toward reporting less. A lifecycle-classed object's stale copy in the primary database is a declared name, so it is not reported. That is the empty table an apply created there before the telemetry sibling was provisioned, which that release's changeset already says it leaves in place.

Generated by Claude Code

…ded served-boot fact and sweeps every planned database

The composition records once, where it decides it, whether its object set
mirrors what `os serve` registers (`servedBoot`), with the reason when it does
not. `collectUnmanagedTables` reads that fact instead of inferring it from
`hostConfigLoaded`, states the recorded reason when it does not run, and reads
the catalog of every driver the plan diffs, the telemetry sibling included.

Claude-Session: https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k
Co-authored-by: Claude <noreply@anthropic.com>
…n artifact project, its reason on the controls, and the sibling sweep

Claude-Session: https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k
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 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 15 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 — 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 a360cee92e2eff7dda2f3e5a2baf8dfd013d77b7 → packageMentionDocs.

Which tree this was computed on

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

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

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

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