Uh oh!
There was an error while loading. Please reload this page.
feat(cli): os migrate plan reports the platform-namespaced tables no declaration accounts for - #13270
feat(cli): os migrate plan reports the platform-namespaced tables no declaration accounts for#13270os-trump wants to merge 3 commits into
Conversation
…es (#13204) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…s the deployment (#13204) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…d directory (#13204) Every compiled file under dist/commands is loaded as a command (oclif.commands.glob), so a helper module there made every 'os' invocation print 'command migrate:unmanaged-tables not found'. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
os-trump
commented
Aug 29, 2026
✅ PM review — ACCEPT once CI is green; ⛔ not while it is running (PR #13270)CI at review time: 14 checks, all Both of the dev's open questions are answered below. ⛔ No rework is owed — do not push in response to this comment. The hard fence — verified here, not taken on reportThe card's one non-negotiable was ⛔ never auto-drop. I re-derived it from the diff rather than accepting the summary:
⭐ And the PR goes one better than my scan: a unit test asserts the rendered text carries no What makes this a strong PR
The file-surface deviation — justified, and I verified the justificationThe dispatch fenced this to "oclif": { "commands": { "strategy": "pattern", "target": "./dist/commands", "glob": "**/*.js" } }Exactly as reported. Every compiled ⭐ This is a reusable constraint worth stating plainly: Decisions on the two open questions — both PM discretion, neither needs the maintainerNeither is a contract change, a security boundary, an ADR-class call, or a gate weakening, so I am ruling rather than escalating. Q1 — prefix source: ✅ OPTION A. Keep Q2 — the Non-blocking observations — ⛔ do not push for these
Owed
Generated by Claude Code |
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 611f67334a3892a0f8fa2f1965fb75ff9344dd5e && git checkout 611f67334a3892a0f8fa2f1965fb75ff9344dd5e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74049254d47bd0edd2a2fcd732dcc01c91504f10 275933f45005aa9ac19da84a0ee5dd1584f05f99 && git checkout -B drift-repro 74049254d47bd0edd2a2fcd732dcc01c91504f10 && git merge --no-ff 275933f45005aa9ac19da84a0ee5dd1584f05f99
node scripts/docs-audit/affected-docs.mjs --json 74049254d47bd0edd2a2fcd732dcc01c91504f10
|
Fixes#13204
detectManagedDrift()diffs the tables metadata declares against the physicaldatabase, so a table nothing declares is not in its input and no plan can ever
mention it. Every object retirement therefore strands its table forever, and the
plan reads clean while it sits there.
os migrate plannow sweeps the physical table catalog once per run and reportsthe base tables that carry a reserved platform namespace prefix and that no
declaration accounts for.
The hard fence
⛔ Nothing is dropped and no drop is proposed. The section names tables and
stops; no DDL is generated, none is suggested, and the module is wired into
nothing that executes DDL. A unit test asserts the rendered text contains no
drop, nodeleteand no--allow-destructive. Removing an existing physicaltable is destructive and hard to reverse — that decision stays with a human.
⛔ This is not
composition.coverage. Coverage says what the plan EXAMINEDof what the deployment declares; this says what EXISTS that no declaration
accounts for. They stay separate lines over separate populations, and a
declared-but-unexamined object's table is deliberately excluded here — coverage
already reports it, and folding the two would make "examined and clean"
indistinguishable from "never looked at".
What was measured before choosing the predicate
The card's suggested
sys_-prefix scan against the managed set alone is notthe predicate. Four families would have been reported falsely, three of them
measured in this tree:
sys_activitydeclareslifecycle.storage.strategy: 'rotation', 14 daily shards.aliasShardBookkeepingcopies the per-table bookkeeping to each shard but never adds it tomanagedObjectFields— only the BASE name is ever a key there. Up to 14 false rows on every plan of everyplugin-auditdeployment.__r+ 6-to-8 digit grammar) before the membership testmanagedObjectFieldsstack.allObjects()) is unioned into the managed setsys_metadata+ its four siblings,sys_migration,sys_migration_journal,sys_metadata_activation,sys_secret, ten objects total — so a real database'ssys_user/sys_session/sys_accountare all reportedcomposition.hostConfigLoaded, the discriminator #12953 kept for consumers; otherwiseunreadablewith the reason_objectstack_sequences, the SQLite rebuild scratch__os_mig_+ the table nameThe prefix comes from
PLATFORM_OBJECT_PREFIXES(sys_,cloud_,ai_)rather than a
'sys_'literal. That module is the repo's own registry ofnamespaces reserved for platform objects, and its header records what a
hand-rolled fourth copy of the prefix heuristic cost last time. This is a
deliberate widening of the card's
sys_*wording, stated here because it is one.Where the sets come from
managedObjectFields, the same mapdetectManagedDrift()iterates, read off the driver the plan diffed. Onesource, so the section cannot disagree with the plan beside it. If that map
cannot be read, the sweep is
unreadable— never an empty set, which wouldreport every platform table as unmanaged.
resolveSeedTenancyExec(engine): that walks the engine for any raw-capabledriver, which on a multi-datasource deployment can be a different database, and
the sweep would then compare database A's tables against database B's managed
set.
SqlDriver.introspectSchema()'s own table-name pass (BASE TABLES only, so arotation base — which is a VIEW — is out;
sqlite_%internals are out). Oneround trip.
introspectSchema()itself is not called: it costs four furtherintrospection queries per table, ~320 on the control plane this card comes from.
null, never a guess.Reverse verification, both directions
Positive and negative controls, on a real booted stack and a real database file
(
unmanaged-tables.integration.test.ts, #12938's fixture shape — a hostobjectstack.config.tswhose object set comes from a plugin):sys_scim_provideris reported;sys_permission_set(declared by the composed plugin),sys_secret(theplatform floor),
_objectstack_sequencesand an application table are not;section goes silent — so "reports only
sys_scim_provider" is not alsosatisfied by a section that reports a fixed string.
Four ablations, each mutation and each restore proven on disk by blob hash
(
git hash-objectagainst theHEADblob), each turning exactly the intendedtest red and no other:
does NOT report the rotation shards of a declared rotation object×does NOT report a DECLARED-but-unexamined objecthostConfigLoadedpremise gateis unreadable when no host config was loaded×Non-SQL drivers
Measured, not assumed:
os migrate plan --database-url memory://qareturns atthe existing
!stack.driverguard, before the sweep. Human mode prints theunchanged warning;
--jsonemits{"error":"no_sql_driver","changes":[]}withno
unmanagedTableskey. Exit 0, no crash, no silent empty sweep.Real command output
--jsongrowsunmanagedTables, always present once a SQL driver was found— including
tables: [], so a consumer can tell "swept, everything is declared"from "never swept".
File surface — one declared deviation, stated
The dispatch fenced this to
packages/cli/src/commands/migrate/**. The helperstarted there and had to move:
package.json'soclif.commandsis{ strategy: 'pattern', target: './dist/commands', glob: '**/*.js' }, so everycompiled file under
commands/is loaded as a command. Measured on the builtCLI, the helper made every
osinvocation printcommand migrate:unmanaged-tables not foundfrom oclif'sfindCommand. It isthe only non-command file that has ever been under
src/commands/. It now livesin
packages/cli/src/utils/unmanaged-tables.tsbesideschema-migrate.ts,schema-migration-plugins.tsandmigrate-occupancy-gate.ts— every othermodule the migrate commands share. New files only; no existing file outside the
fence was touched.
Verification
pnpm --filter @objectstack/cli typecheckexit 0 — andtsc --noEmit --listFilesconfirms all three new files are in the program (a
typecheckthat excluded thetests would be a green verdict over source nothing read).
Tests, at the final commit
275933f4:19 files / 151 tests passed— the twonew files, all of
src/commands/migrate, bothmigrate-plane2e spawns, and theschema-migratehost-composition / readonly-probe / deferred-ddl integrationsuites.
Gate union re-derived for the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(34 gatesrun, exit codes captured before any pipe): all green, including
check:test-source-alias(which caught a real defect here — a clocked-windowimport()in the integration test, fixed with a module-top side-effect import),check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:type-check-debt --re-measure,check:cross-package-test-inputs,check:where-matcher,check:engine-double-contract,check:nul-bytes. Repo-widepnpm lint(eslint . --no-inline-config) exit 0 — run whole, not narrowed.Two gates are NOT MEASURED, both by their own refusal and neither a red:
check-test-completeness(needs a savedturbo run testlog; its own text saysto record NOT MEASURED when the family runs it with no argument) and
scripts/pm/check-half-states.mjs(needs a real GitHub credential; thiscontainer carries the proxy placeholder).
Generated by Claude Code
Generated by Claude Code