fix(runtime,cli): a plain os dev self-heals safe drift and provisions the telemetry sibling on the standalone stack (#21733) - #21766
Conversation
…emetry sibling on the standalone path A plain `os dev` on a non-host config composes the standalone stack, whose `default` datasource definition never carried `autoMigrate: 'safe'` — that decision lived only inline in the CLI's config-load fallback. Move it to ONE home in `@objectstack/runtime` (`devAutoMigrateConfig`) that both hosts and the telemetry sibling read, keyed on an explicit `dev: true` so a one-shot command boot never auto-applies whatever NODE_ENV says. Extract the telemetry provision into `provisionTelemetryDatasource` and run it on the standalone path too. Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
…ence, host parity and the telemetry provision Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
…f-heals and provisions telemetry; production does not migrate Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
…l and telemetry sibling Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
… every spawn a readable env literal Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 43 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 de86f6ffc84aa9f350bac87462ae51b521bd3651 && git checkout de86f6ffc84aa9f350bac87462ae51b521bd3651
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b7a13c762fc39bfdf5b1d393b8334eb53a22e498 3e2810854492a6d573a1288bf26af756d5382059 && git checkout -B drift-repro b7a13c762fc39bfdf5b1d393b8334eb53a22e498 && git merge --no-ff 3e2810854492a6d573a1288bf26af756d5382059
node scripts/docs-audit/affected-docs.mjs --json b7a13c762fc39bfdf5b1d393b8334eb53a22e498
|
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
…; DefaultDatasourcePluginOptions.dev arms the step-down only Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-Authored-By: Claude <noreply@anthropic.com>
Contract reviewServed-tier: PR #21766 for card #21733, judged on the net diff against the merge base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #21733
Clause-②: yes (widening)
What this changes
A plain
os devon a non-host config (every fresh scaffold) boots through the standalone stack, and that path now gets the two dev behaviourscontent/docs/deployment/cli.mdxalready documents:autoMigrate: 'safe') is applied. The card's staged non-NULL-safe unique index comes back NULL-safe, and the drift and plan lines saying "auto-applied at boot under dev autoMigrate: 'safe'" are now true. Their wording is unchanged.DB.telemetry.EXTsibling datasource is provisioned next to a file-backed SQLite primary under the sameresolveTelemetryDbPathrule.OS_TELEMETRY_DB=0opts out.Production boots and one-shot command boots do not change.
Route (A2): one decision, in
@objectstack/runtimepackages/runtime/src/dev-auto-migrate.tsexportsdevAutoMigrateConfig(driver, dev). It decides two things in one place:autoMigrate, i.e. sqlite, postgres and mysql.defaultdefinition (standalone-stack.ts);resolveStorageDefinition(storage-driver.ts, the host-config path);service-datasourceandspecare out of lane. The move adds two exports to@objectstack/runtime's only entry,devAutoMigrateConfigandDevAutoMigrateConfig. That is this PR's published-surface widening, henceClause-②: yes (widening)and aminoron runtime (see the changeset).'safe'to exactly sqlite, postgres, mysql and the dev-default:memory:sqlite, and none to sqlite-wasm, mongodb or turso. Two pins hold this:dev-auto-migrate.test.tsholds the helper's set equal to the spec contract;dev-self-heal-host-parity.test.tsdrives both real entry points for all 6 kinds × dev/production. All 12 rows agree.One-shot fence (A3)
bootSchemaStack(schema-migrate.ts, everyos migrate */os meta */os secret *boot) passes nodev. SofactoryDevfalls back toNODE_ENV === 'development'.cfg.dev === trueonly. The step-down keeps itsNODE_ENVdefault.os dev,os serveandos startalways passdevexplicitly, fromserve.tsand throughcreateDefaultHostConfig.factoryDevunderNODE_ENV=development:expected [] to include 'safe:recreate_index');os migrate planstayed write-free, because its deferred DDL never reaches the reconcile (pin green).schema-migrate.dev-self-heal-fence.integration.test.ts:safeand leaves the staged file byte-identical;bootSchemaStackleaves the drift in place;dev: true) heals the same file, as a positive control.Telemetry on the standalone path (A4)
serve.tsbecameprovisionTelemetryDatasource(utils/telemetry-datasource.ts). Both serving paths call it through one closure:resolution.sqliteFilePath;standaloneTelemetryPrimary(input), which is the runtime's own pre-bootresolveStandaloneDatabaseover the same input the stack was handed. Only the nativesqlitekind counts, as on the host path.servedSqliteFilePathon this path is still read from the booted driver, and the plugin roster gainsTelemetryDatasourceexactly as on the host path.standalone-stack*,default-host*, the CLI tests or ADR-0057/0062. ADR-0062 records the telemetry sibling as a pre-builtDriverPlugin, and it stays one.Reproduction (A1), origin/main
8cbba54491, built CLI (bin/run.js)The scaffold is one object
scaf_itemwithqa_code: { unique: true },sharingModel: 'private', and no plugins.os dev --no-watch -d file:A.db) created the NULL-safe indexCREATE UNIQUE INDEX `uniq_scaf_item_organization_id_qa_code` ON `scaf_item` (COALESCE(`organization_id`, '__global__'), `qa_code`). NoA.telemetry.dbwas created.CREATE UNIQUE INDEX uniq_scaf_item_organization_id_qa_code ON scaf_item (organization_id, qa_code).os migrate plan --database-url file:A.dbthen listed✓ scaf_item [uniq_…] [recreate_index]under Safe, and the file was byte-identical afterwards.os devrestarts each logged[schema-drift] … (auto-applied at boot under dev autoMigrate: 'safe'), and the index stayed bare both times.OS_MODE=offlogged[schema-drift] auto-reconciled recreate_index on scaf_item.organization_id, the index came back NULL-safe, andA.telemetry.dbwas created.os devlogsauto-reconciled recreate_index, the index comes back NULL-safe, andA.telemetry.dbis created. It holdssys_audit_log,sys_job_run,sys_notificationand 4 more tables.Tests
Full suites and lint ran at
c20d26dd76(round 1). Round 2 mergedorigin/mainb7a13c762fand reran the targeted set at3e28108544, listed under the table.pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2(full runtime suite)pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2(full)… --project integration --maxWorkers=2 --shard=1/4 … 4/4(full, 93 files)pnpm --filter @objectstack/runtime typecheck/@objectstack/cli typecheck(incl. test layers)pnpm lint(full,eslint . --no-inline-config)3e28108544: exit 0, 104 sRound 2 at
3e28108544, after the merge and a full turbo rebuild of./packages/*and./packages/*/*(71 tasks):pnpm --filter @objectstack/runtime typecheckand@objectstack/cli typecheck, including the test layers: green.src/default-datasource-plugin.test.tsandsrc/dev-auto-migrate.test.ts: 2 files, 19 passed.The new pins:
packages/runtime/src/dev-auto-migrate.test.ts: contract equality, plus thedefaultdefinition under dev, production, and theNODE_ENV=developmentdefault.packages/cli/src/utils/dev-self-heal-host-parity.test.ts: both hosts, 6 kinds × dev/production.packages/cli/src/utils/schema-migrate.dev-self-heal-fence.integration.test.ts: the one-shot fence, in its own file.packages/cli/src/utils/telemetry-datasource.provision.integration.test.ts: the provision, theOS_TELEMETRY_DB=0opt-out, production opt-in only by explicit path, andstandaloneTelemetryPrimary.packages/cli/test/dev-standalone-self-heal.integration.test.ts: the card's restart pin on the built CLI.os devwithOS_TELEMETRY_DB=0: NULL-safe index, no sibling;auto-reconciled,A.telemetry.dbpresent;os serve(NODE_ENVunset) on the restaged file: the drift is reported and left bare.Reverse verification (A5):
scripts/ablation-replace.mjs, rebuilt,ablation-dist-preflightboth legsAll three legs follow the same sequence:
dist/carries the marker (preflight exit 0).git diff HEADis empty.--absent: the marker is gone fromdist/and the tree is clean.standalone-stack.ts/serve.ts)config: { ...driverConfig, ...devSelfHeal }→...(String('ABLATED_21733_A') ? {} : devSelfHeal)69295ae4dcb6→3a20c5f19924→69295ae4dcb6storage-driver.test.ts(host path); runtime contract, production and NODE_ENV casesstandaloneSqlitePrimary = await standaloneTelemetryPrimary(standaloneInput);→String('ABLATED_21733_B') ? undefined : …d5486ac90717→42870a5cbc82→d5486ac90717devAutoMigrateConfig(dbDriver, cfg.dev === true)→… String('ABLATED_21733_C') ? factoryDev : cfg.dev === true69295ae4dcb6→22ebf7f09362→69295ae4dcb6os migrate planbyte-identical; serving positive controlGates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat3e28108544(merge baseb7a13c762, 14 paths) derives the same 86 families as round 1. All 85 runnable families were rerun on that head and passed. That includescheck:changeset-no-major --base origin/main(nomajor) andcheck:adr-0087-registration(no declared-breaking changeset).Round-1 notes, at
c20d26dd76:check:dual-build-cjs-loadsandcheck:i18n-coveragefirst answeredPREREQUISITE NOT MET(exit 3) on unbuilt packages. They were rerun green after building those packages.check:cli-test-child-envwent red on the new built-entry spawner and was rerun green. The fix is in the second deviation below.check:pm-dispatch-gates. Its self-test and its bare check each ran past the 10-minute foreground cap at 590 s on this shared box, with every printed case passing. It gatesscripts/pm/dispatch-gates.mjs, which this diff does not touch. CI runs it.check:pm-dispatch-gatesis carried NOT MEASURED into round 2. Its inputs (scripts/pm,.github,.claude, the rootpackage.json) are byte-unchanged betweenc20d26dd76and3e28108544.--ranat3e28108544reports86 derived famil(ies) accounted for — 85 run, 1 NOT-MEASURED.git grep autoMigrateoverpackages/spec/liveness/**and*.ledger.*found 0 hits; the control termsqlitehitsdatasource.json. No symbol was renamed or removed.Deviations for the seat
@objectstack/runtimegains two exports.devAutoMigrateConfigand itsDevAutoMigrateConfigtype, on the package's only entry. This is the widening declared above.StandaloneStackConfigSchemaor to any other exported type or schema, and no accept set moves.devkey's TSDoc now states its narrower self-heal meaning: an explicittrueonly.minoron@objectstack/runtimeandpatchon@objectstack/cli.scripts/check-cli-test-child-env.mjscensus. Its pinned built-entrypoint population (formerly "exactly the six files") now admitsdev-standalone-self-heal.integration.test.ts, with a comment saying why. The production leg is only reachable throughbin/run.jswithNODE_ENVunset. The gate's 152 self-test cases pass.b7a13c762fcame in through a merge commit (07745648c0, no rebase), with no conflicts. One file overlaps,serve.ts, where fix(auth): TOTP enrollment names the deployment app name as its issuer, not Better Auth #21752 adds the AuthPluginappName; its hunks are disjoint from this diff. After the merge, the branch's diff againstmainwas the same 13 files with the same +1134/−68 as before; round 2's R4 TSDoc edit makes it 14 files, +1143/−69. The rebuiltserve.jscarries both sides.Acceptance notes
DefaultDatasourcePluginOptions.devTSDoc (packages/runtime/src/default-datasource-plugin.ts), comment only, fixed in round 2. It used to saydevarms the "self-heal passthroughs". It now says whatdevarms, the sqlite step-down (better-sqlite3 → wasm auto-fallback doesn't cover the persistent-file / --artifact dev path (only the zero-config :memory: branch) #2229), and that the self-heal rides in the definition'sconfig.autoMigrate, whichdevAutoMigrateConfigdecides.createDefaultHostConfigbranch (os startwith no config,os dev -a) gets both behaviours too. Its telemetry primary is resolved from the same input handed tocreateDefaultHostConfig. The empty-boot stub declares no datasource, so the database answer cannot differ. This is commented at the call site.Generated by Claude Code