Repository navigation
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
Conversation
…he stack The auth family behind serve's auth gate, the provider of every capability serve's resolver mounts (requires + the always-on slate) and the REST API plugin, each for its declarations only; host plugins entries by serve's entry rule; the pinyin-search stamp from the loaded config. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…om the rotator's facts A rotation-declared object (ADR-0057 P2) lives as its current shard plus a read view under the base name; the preview asked hasTable(base), false for a view, and listed create_table on every plan while the flush created nothing. The current shard and the read view present now means nothing is pending. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…arity per app shape The hotclm shape over a legacy sys_account (plan names both drops, apply performs them, account-issuer reads zero, a re-plan is in sync), the parity pin against a real os serve provision per shape, unit cases for the served composition, the module header, the changeset, and account-issuer's clean prescription made true whether or not the column is still there. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…rts its config loads Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
…nd the host-config shape carries its own metadata Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 35 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 28df4c1a4549354f9793da765c3df44eca893310 && git checkout 28df4c1a4549354f9793da765c3df44eca893310
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 25be87612d9a61f9f921ab4e993822669f04b321 5bdfba6bc1e74f7ef36bb09424594d7f64c7dfa7 && git checkout -B drift-repro 25be87612d9a61f9f921ab4e993822669f04b321 && git merge --no-ff 5bdfba6bc1e74f7ef36bb09424594d7f64c7dfa7
node scripts/docs-audit/affected-docs.mjs --json 25be87612d9a61f9f921ab4e993822669f04b321
|
…rved platform included Its documented contract is the plan's own unmapped_column findings over the plan's object set; with composeServedPlatform on plan/apply only, a platform object the plan reports on (sys_account) was OBJECT_NOT_FOUND here. Claude-Session: https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22506
Clause-②: no
os migrate plan/applystart composing what the boot composes, so a retired column the published upgrade step names as a destructive drop becomes one. No accepted input, flag, key or export changes.What changes
os migrate planandos migrate applyonly examine the objects their own boot registers. That boot stopped at the stack: its config, its metadata and the platform floor. Nothingos servemounts around a stack was there. This is the third time the family has surfaced (#12938, #21732), and each earlier fix grew one list. This PR composes through the rulesserveitself reads and pins parity with a realserveboot.packages/cli(utils/schema-migration-plugins.ts,utils/schema-migrate.ts,commands/migrate/plan.ts,apply.ts). With the newservedPlatformoption, whichplanandapplyset (andunmapped-columns, below), the composition adds:serve's auth gate. The gate isresolvePlatformAuthComposition(@objectstack/core, feat(cli): os migrate security-catalog-overlays — list, and with --apply delete, the environment rows a v18 cold boot refuses #22523's shared rule), fed byresolveStackTiersandresolveAuthSecret. Plugin-auth's objects come throughcreateIdentityObjectsPlugin(),AuthPlugin's declaration twin: the same manifest under the same package id, and no authentication. The security plugin and the audit plugin are composed beside it. The family sits whereserveregisters it, ahead of the host plugins.servemounts. That means the stack'srequires, read byserve's rule, plusPLATFORM_ALWAYS_ON_CAPABILITIES, asos servewith no--presetcomposes them. Each is looked up throughCAPABILITY_PROVIDERSand skipped when already held, byprovidesCapability(@objectstack/core, feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) #22381's shared rule). It replaces the cli: os migrate plan / apply cannot boot the stock showcase or a fresh blank app — Dependency 'com.objectstack.service-automation' not found for the connector plugins #21732 "only when hard-depended on" narrowing for these two commands.servecomposes it on every boot, and itsinit()declaressys_import_job.pluginsentry, byserve's entry rule (materializeStackPlugin,@objectstack/core). A string is loaded host-anchored throughServe.importConfigPlugin, and a bundle is wrapped as anAppPlugin. Before, a string entry vanished silently and a bundle reached the kernel raw. An entry that cannot be loaded makes the config unloadable, so the os migrate plan exits 0 when the host config exists but could not be loaded — the one remaining UNMEASURED shape after #12938 #12953 refusal applies. This rule applies to every caller of the host composition.stampSearchPinyinEnabled, the one helperservecalls). A config-only project used to plan without the__searchcompanion columns its boot provisions. Measured onexamples/app-crm: 49 destructivedrop_column __searchentries before the stamp, none after.Posture. Every provider is composed through a new
composeProviderForDeclarations: itsinit()runs, itsstart()does not, and it registers no lifecycle hook,kernel:readyincluded. That is stricter than the host-plugin posture, for a measured reason. With the host posture,MessagingServicePlugin's notification and outbound-HTTP dispatchers (armed fromkernel:readyhooks itsinit()registers) ticked insideplan. The write guard comes off when the bootstrap ends, so on a database holding pending deliveries a dry run could have sent them. With this posture, zero dispatcher lines. Theautomationtoken keeps its #21732 inert posture.When no auth secret resolves, outside development, the plan says it did not compose the auth family and names the command that does: re-run with the deployment's
OS_AUTH_SECRETexported.os migratereads no.envfile.packages/drivers/driver-sql(previewDeferredSchemaWorkonly, per the surface extension6091922191). A rotation-declared object (ADR-0057 P2) lives as its current shard (__rYYYYMMDD) plus a read view under its base name. The preview askedhasTableof the base name, which is false for a view, so it listedcreate_tableon every plan. The flush then took the sync's rotation branch and created nothing. Once the composition registers the audit plugin'ssys_activity, that would have been a finding no apply can clear on every auth-enabled deployment. The preview now answers from the rotator's facts: its shard key for now and the two names insqlite_master. When the current shard and the read view are both present, nothing is pending. The rotator,syncSchema's rotation branch andflushDeferredSchemaDdlare untouched.commands/migrate/unmapped-columns.ts(review round, claim widened in6092886177). ItsbootSchemaStackcall passescomposeServedPlatform: truetoo, and its comment names it; nothing else in the command changes. Its documented contract (content/docs/deployment/cli.mdx: "the same differ, the same boot") is the plan's ownunmapped_columnfindings over the plan's object set. With the flag onplan/applyonly,os migrate unmapped-columns --object sys_accountansweredOBJECT_NOT_FOUNDfor a platform object the plan reports on.commands/migrate/account-issuer.ts. A one-line change: the clean prescription is now true whether or not the column is still there. Whileos migrate planstill lists the drop, runos migrate apply --allow-destructive; once it lists none, the retirement is done.Readings
All readings go through the public doors. The hotclm shape is an app config with
requires: ['auth']and no plugins, over a SQLite database a realos serveboot created, then given the legacysys_account.issuercolumn plusuniq_sys_account_issuer_account_id.main4638625e07os migrate plansys_accountlisted as an undeclared platform table; 0 driftsys_accountdeclared; 2 destructive:drop_column sys_account.issuer,drop_index uniq_sys_account_issuer_account_idos migrate apply --allow-destructive --yesos migrate account-issuerapply --allow-destructiveagainos serveboot[schema-drift] sys_account …lines prescribingapply --allow-destructivesys_accountdrift lineNo-secret production leg (in-process,
NODE_ENV=production, noOS_AUTH_SECRET):sys_accountis not registered, and the note names the remedy above.Parity per example app (a real
os serve --no-serverboot withOS_MIGRATE_AND_EXIT=1provisions a fresh database, thenos migrate plan --jsonruns on it):examples/app-showcase, the real host config, at47823309b4: drift and pending empty; unmanaged onlysys_packages; 105 of 107 registered objects examined, the other 2 federated (correctly out of scope). The 107 boot tables are the 105 plussys_packagesand_objectstack_sequences, the driver's internal auto-number table.examples/app-crm82/82,app-todo77/77 andapp-multi-package75/75: drift empty, unmanaged onlysys_packages. These were measured before the driver fix, so each still listed thesys_activityphantom that the driver pin now clears.This composition widens the telemetry-sibling mismatch from 1 object to 7
A development boot, or one with
OS_TELEMETRY_DBset, keeps lifecycle-classed objects in a siblingobjectstack.telemetry.db(ADR-0057 §3.6,provisionTelemetryDatasource). The one-shot migration boot does not provision that database, so it examines those objects against the primary one.Measured with a development boot of the hotclm shape: the boot's
objectstack.telemetry.dbholdssys_audit_log,sys_activity,sys_metadata_audit,sys_job_run,sys_notification,sys_notification_deliveryandsys_http_delivery.main,os migrate planlists one of them (sys_metadata_audit) ascreate_tableagainst the primary, andapply --yescreates it there.planlists all seven:sys_activity,sys_audit_log,sys_http_delivery,sys_job_run,sys_metadata_audit,sys_notification,sys_notification_delivery.Production boots without
OS_TELEMETRY_DBare unaffected: they keep these objects in the primary database, and the plan matches it. The seat keeps the telemetry sibling out of this card (its ruling6091922191) and files it at ACCEPT.Pins
src/sql-driver-deferred-ddl.test.ts, "a rotation-declared object previews from the rotator's facts". Already sharded previews no work; the never-sharded control previewscreate_table; after a flush, the next preview is empty.306a75dcf2withsql-driver-rotation.test.ts: 2 files, 23 passed.sql-driver.tsat4638625e07written to the tree only: 2 failed (already sharded, converges), 16 passed (the control among them).git diff HEADempty, hash back to theHEADblob.src/commands/migrate/apply.account-issuer-retirement.integration.test.ts. It runs in-process over asys_accountbuilt by the driver from today's definition, then given the legacy column, index and one issuer-carrying row. The four cases: plan names both drops; apply performs them; account-issuer reaches zero (scanned 1); a re-plan is in sync, with no phantomsys_activity.src/commands/migrate/plan.boot-parity.integration.test.ts. One case per app shape: an app withrequires: ['auth', 'automation'], a host config with plugin instances beside its own metadata, and a compiled-artifact standalone stack.sys_packages, which with pending empty makes equal sets (boot ⊆ plan); and, where the sweep runs, unmanaged onlysys_packages.sys_packagesis named as raw DDL fromPackageServicePlugin.start(), outside the object set.scripts/ablation-replace.mjs:composeServedPlatform: falseinplan.ts(anchor 1 to 0, blob changed) turns all 3 red, with boot tables 78 / 76 / 75 against examined-plus-raw 10 / 11 / 10. Restore verified.src/commands/migrate/unmapped-columns.integration.test.ts, "a platform object the plan declares (os migrate apply --allow-destructivecannot dropsys_account.issueron a 17.4.0-created SQLite database, whileos migrate account-issuerand the boot's schema-drift line keep prescribing it (17.7.0) #22506)". The seed adds asys_accounttable built by the driver from today's definition, plus the retiredissuercolumn and one row; on an auth-enabled stack--object sys_account --jsonexits 0 and readsissuerfor that row, the same column setos migrate plan --jsonreports asunmapped_columnfor the table. With the flag removed (scripts/ablation-replace.mjs, anchor 1 to 0): that case red with{"error":"Object 'sys_account' not found","code":"OBJECT_NOT_FOUND"}, exit 1, the other 9 cases green; restored (blob back toHEAD,git diff HEADempty).src/utils/schema-migration-plugins.test.ts, "servedPlatform (os migrate apply --allow-destructivecannot dropsys_account.issueron a 17.4.0-created SQLite database, whileos migrate account-issuerand the boot's schema-drift line keep prescribing it (17.7.0) #22506)" and "composeProviderForDeclarations (os migrate apply --allow-destructivecannot dropsys_account.issueron a 17.4.0-created SQLite database, whileos migrate account-issuerand the boot's schema-drift line keep prescribing it (17.7.0) #22506)". They cover the family behind the gate, ahead of the host plugins; the slate providers and the REST API plugin; a host-supplied provider winning; no secret, with the remedy named; a stack mounting its ownAuthPlugin; the bundle entry rule and the unloadable string entry; the pinyin stamp; the control withoutservedPlatform; and the provider posture against the host posture.Verification
Review round, head
5bdfba6bc1(unmapped-columnsboots the plan's object set):pnpm --filter @objectstack/cli typecheck): exit 0.vitest run --project unit):unmapped-columns.test.tsplusschema-migration-plugins.test.ts: 2 files, 67 passed.unmapped-columns.integration.test.ts,plan.boot-parity.integration.test.tsandapply.account-issuer-retirement.integration.test.ts: 3 files, 17 passed. The flag-removed leg is in Pins.dispatch-gates --commandsderives the same 69 families; all 69 exit 0 at5bdfba6bc1, withcheck:type-check-debtandcheck:dual-build-cjs-loadslast.--ranreads 69 derived, 69 run, 0 NOT-MEASURED (derived from recorded exit codes).First round, head
47823309b4:The source has been identical since
143f14e192; later commits touch two test files only.pnpm --filter @objectstack/cli typecheck,tsc --noEmitpluscheck:test-typecheck) at143f14e192: exit 0.vitest run --project unit) at143f14e192: 276 files, 4085 passed. At47823309b4,schema-migration-plugins.test.tsplustest/exit-signal.pin.test.ts: 2 files, 160 passed.src/commands/migrate/,src/utils/schema-migrat,unmanaged-tables,platform-migrations-arming,artifact-boot-migration,sqlite-occupancy,test/migrate-,test/dev-standalone-self-heal.143f14e192: 34 files, 317 passed, 2 skipped, 2 failed. Both failures were in the parity pin, fixed in47823309b4, where the parity file passes 3/3.planincluded, still moves no byte.47823309b4, with three pieces of evidence:eslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minus the globalNEVER_LINTED;pnpm lintinvokes ESLint (--no-inline-config,--format json), give 10 files, 0 errors and 0 warnings;parserOptions.project, no typed rules), so this diff cannot move the verdict of any untouched file.dispatch-gates --commandsderives 69 families from this branch; all 69 exit 0 at47823309b4.--ranreconciliation reads: 69 derived, 69 run, 0 NOT-MEASURED (derived from recorded exit codes).check:i18n-coverageandcheck:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, from showcase-only and 8 unrelated packages withoutdist/in this worktree). Both re-ran green after those builds.check:type-check-debtandcheck:dual-build-cjs-loadsran last.git grepformigrate plan|apply,MigratePlan,MigrateApply,bootSchemaStackandbuildSchemaMigrationPluginsoverpackages/qa/dogfoodexits 1 (zero hits).Acceptance notes
os migratereads no.envfile, and the absence of--preset/--devonplan/applystay out of this card, by the seat's ruling..env, measured onmain: a fixture whose.envsetsOS_DATABASE_URLgets a plan of.objectstack/data/objectstack.db, whileserveloads that.env.--preset minimalgets the always-on slate examined.utils/unmanaged-tables.tsstill reports itselfunreadableon a project with no host config, saying the composed set is "the compiled artifact plus the platform floor". Forplan/applythat reason is now false (the served platform is composed); the conservative verdict is unchanged. The file is outside this card's surface; carrier: none.sys_packages(raw DDL) and_objectstack_sequences(the driver's auto-number table) are tables no object declares. The parity pin names the first; its fixtures declare no auto-number field.CORE: Core service missing … authduringplan/applyonce the identity objects are composed withoutAuthPlugin. This is cosmetic stderr noise.create_table. That is real work the flush then does.formatAccountIdentityPreflightReport(plugin-auth) still prints "sys_account.issuer is safe to drop." when the column is already gone. Onlyaccount-issuer.ts's own prescription changed.origin/mainmoved 7 commits since the base4638625e07, including the dispatch-gates rework (tooling(pm): dispatch-gates — the hand-written tables move out as data, the self-test out as a module with fast/slow tiers, derivation byte-identical #22531). None of them touches this diff's files, so nothing was merged.Generated by Claude Code