fix(spec,cli,runtime,service-datasource)!: one driver vocabulary — os start and os migrate stop disagreeing (#6345) - #6910
Conversation
…lias, regenerated artifacts
…ver-alias-single-table
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 123 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…k ellipsis, ADR-0122 alias, colliding alias probes
…ver-alias-single-table
…ver-alias-single-table
|
Pushed Three of the four were in the one file this PR adds,
On #4, the verification you asked for rather than the generator's exit code. I diffed my regenerated snapshot against Zero removals is the assertion that matters — that is the one the stale- The third break, found by the sweep rather than by CI
Re-measured on the re-merged tree
Still in draft; ready-flip and queue are yours. Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31297062268 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…ver-alias-single-table
|
Heads-up from the drivers lane — this PR's tree currently reds the #6860 pin, and the failure surfaces on neighbouring PRs rather than on this one, so it is easy to miss. Queue build 31297272050 (PR #6932, an unrelated Why this PR is the likely owner. That pin derives its expected set by scraping every lowercase quoted literal out of That is only a test failure if the new behaviour is intended; if unrecognized kinds now resolve or throw in production where they previously returned Probable cause: the pin landed in #6913 (merged), and this PR does not touch it — its file list covers The pin's own docstring anticipates you explicitly:
So updating the pin alongside the vocabulary looks in-scope for this card rather than a drive-by. No action requested of you here beyond awareness — I have not requeued anything of yours, and #6932 has since re-entered the queue without this PR ahead of it. Generated by Claude Code |
…zed spelling is not a driver kind #6345's CLI-side refusal of an explicitly-named unknown driver reused UnsupportedDriverError with the operator's RAW TOKEN in driverType. The #6860 pin uses resolveStorageDefinition as its oracle and reads driverType out of that error, so its deliberately over-broad candidate scan started reporting every lowercase literal in storage-driver.ts ('safe', 'on-disconnect', 'factory', 'string', ...) as a driver kind. The allowlist itself was already correct: #6860 landed the canonical seven (sqlite, sqlite-wasm, turso, postgres, mysql, mongodb, memory), mongodb included. start.ts and dev.ts are therefore untouched. UnsupportedDriverError now carries 'recognized', defaulting true so the pre-#6345 turso-with-no-URL call sites keep their meaning, and the pin returns null for the unrecognized case. The assertion is unchanged: both sides still derived, still required to be equal. Also regenerates spec-changes.json / protocol-upgrade-guide.md, which the merge brought stale (os-regen guard).
…ver-alias-single-table
|
Queue failure fixed — pushed Why two green PRs went red togetherThe } catch (err) {
if (err instanceof UnsupportedDriverError) return err.driverType; // ← beforeThat catch was written for one case: turso with no URL — recognized, unusable, and #6345 taught the CLI to refuse an explicitly-named unknown driver (the CLI half of "both hosts refuse the same input", replacing a silent fall-through to the dev SQLite default). I raised the same Not a vocabulary drift — a poisoned oracle. No allowlist could have equalled that set. Correcting the diagnosis
Measured: that is a no-op. #6860 already landed the canonical seven, options: ['sqlite', 'sqlite-wasm', 'turso', 'postgres', 'mysql', 'mongodb', 'memory'],
The fix
The assertion is untouched — both sides still derived, still required to be equal. I repaired the derivation the pin's own docstring describes ("a recognized kind that cannot become a definition still counts as recognized"); an unrecognized token was never in that category. The discriminator lives on the error, not in the test, so the pin keeps asking the resolver what a token means rather than growing its own opinion. Both files carry a comment naming this incident. I did not take route (b) (deriving the allowlist from the shared table). The pin now enforces exactly the property that derivation would enforce structurally — it just proved it by catching a real drift within hours — and expanding a 40-file PR during a queue incident is the wrong trade. It stays follow-up #1. Proof it still bites (not silenced)
Re-verified on the re-merged tree (
|
|
Queue steward — yielding to the lane (round 71, audit only). Recording the queue-side view of this failure for the audit trail. No requeue, no merge, no code, no claim change — the lane got here first and is already acting. Signature (full log archive via check-run annotations, not a tail — SKILL notes 7):
Blast radius — one head, four downstream victims. Queue generations in the 05:26Z–06:20Z window:
All five failures carry the byte-identical signature above. The controlled comparison is clean: base Four-branch verdict: not a known flaky, not a fixed-signature recurrence — a real regression. No row in any of the four #5810 ledgers matches, and the steward is not requesting one: a deterministic regression with a lane fix in flight is not what the ledger is for (it exists for signatures that must be requeued as-is while unfixed). Yield (SKILL "入队与落地 B", bidirectional yield). Read this PR's recent comments before acting: the drivers lane flagged it at 05:58:57Z and Generated by Claude Code |
Fixes #6345
Implements the maintainer's ruling archived in comment 5229196310 — route C, plus the four calls it settled. The predecessor session's stop report (5229178650) is the measurement baseline; every number below was re-measured on this merged tree, not carried over.
The defect, re-measured on the merged tree
One environment variable had two answers. Driving the real entry points —
resolveDriverType+resolveStorageDefinitionforos start,resolveStandaloneDatabaseforos migrate— 10 of 21 spellings disagreed:sql,wasm,wasm-sqlite,postgresql,pg,mysql2,mongo,mingo,in-memory,libsql— CLI accepts, standalone refuses.One refinement to the predecessor's table. It listed
tursoas disagreeing andlibsqlas agreeing. Measured with a matching URL present it is the other way round:tursoagrees (both acceptlibsql://…), andlibsqldisagrees (a CLI-only spelling). The count is 10 either way; the composition differs because turso's asymmetry lives in fork 2 (no URL), not in the vocabulary.What landed
Fork 1 — one vocabulary (route C)
packages/spec/src/data/driver/config-registry.zod.ts's flatRecordbecomes one table:id/aliases/contractOnlyAliases/hasLocalDefault.BUILTIN_DRIVER_IDS,DRIVER_ID_ALIASESandresolveDriverIdare projections of it;BUILTIN_DRIVER_IDSkeeps its exact tuple type via a homomorphic mapped type, so the api-surface delta is purely additive — nothing removed or renamed.Both hosts now resolve through
resolveDatabaseDriverId. The standalonedatabaseDriverconfig key accepts the same aliases as the env var, so the fork cannot relocate to inside one host.On the ruling's
contractId?column: it collapses to nothing oncemongo→mongodblands and turso gets a contract. A column that would beundefinedon every row is the inert declaration this repo removes rather than ships, so it is not in the table. The distinction it was meant to carry is carried bycontractOnlyAliases, which is non-empty.sql/wasmin,sqlite3/better-sqlite3/mariadb/inmemoryout — the ruling's union principle applied. The four staycontractOnlyAliases: they keep resolving a config contract (dropping that would silently un-validate a storeddriver: 'sqlite3'row) whileresolveDatabaseDriverIdrefuses them as a boot selection.One consequence derived rather than found itemised. An explicitly named unknown driver is now refused on the CLI side too.
os dev --database-driver sqlite3previously fell through to the dev SQLite default and booted in silence, whileos migraterefused it by name. "Both hosts answer the same for the same input" cannot hold with one side silently booting — so it is treated as priced by the ruling.''(nobody chose) keeps its old answer; a non-empty value can only have come from an operator, since URL inference yields a canonical id or''.mongo→mongodb— the full blast radius, itemisedspec/data/driver/config-registry.zod.tsDRIVER_CONFIG_SCHEMAS, JSON-schema mapspec/data/driver/mongo.zod.tsMongoDriverSpec.idspec/conversions/registry.tsDATASOURCE_CONFIG_KEY_ALIASESkeyservice-datasource/driver-catalog.tsDRIVER_CATALOG.id— what Studio writes intodatasource.driverservice-datasource/default-datasource-driver-factory.tsservice-datasource/datasource-pool-support.tsruntime/resolve-project-database.tsdatasourceUrlOfcasespec/kernel/manifest.zod.ts.describe()api-surface/,authorable-surface/data.json,json-schema.manifest/data.json,spec-changes.json,liveness/datasource.json,docs/protocol-upgrade-guide.md,content/docs/references/**,skills/**/_index.md, strictness-ledger countsADR-0087 D2, as ruled — ledger entry + converter, not a D3.
datasource-driver-mongo-to-mongodb(toMajor: 17, live load path) converts storeddatasource.driver: 'mongo'→'mongodb', registered inmigrations/registry.ts'sconversionIdswith a prose paragraph in the step-17 record. Live rather than retired becausemongois still a legal alias.Deliberately NOT renamed:
SqlDialect's'mongo'member indata/type-compat.ts. That names the type system of an external schema being introspected, alongsidesnowflakeandbigquery, and is never adatasource.driver.tursogets a config contract — and who read the{known:false}New
packages/spec/src/data/driver/turso.zod.ts. Keys drawn from whatTursoDriverConfigactually reads, so closing a gate does not open an ADR-0049 one (client,pool,schemaMode/readOnlydeliberately absent).DatasourceSchema→reportDriverConfigIssuesconfig;{ token: … }now gets a rename hint instead of connecting unauthenticateddatasource-admin-service.assertValidConfig(Setup wizard)DRIVER_CATALOGsqlite-wasmdriverReadsDeclaredPooltrue(unknown-id branch)true(not-rejected branch) — verdict unchangedcreateDefaultDatasourceDriverFactory().supports()isresolveDriverId(id) !== undefined, andcreate()'s last arm was an unguardedmemoryfall-through. Making turso aBuiltinDriverIdtherefore made the open-core factory claimsupports('turso') === trueand then silently build anInMemoryDriver— a libSQL datasource that accepts writes, reports success and loses everything. Fixed in-surface: an explicittursoarm (the same lazy-import shapemongodb/sqlite-wasmalready use), plusmemorypromoted to an explicit arm with anever-typed exhaustiveness stop, so the next builtin cannot inherit the trap.Fork 2 — the typed refusal, all 8 cells
os startbeforeos migratebeforepostgresconfig.url === undefined→pgpicks its own localhost:5432file:/data/objectstack.dbmysqlconfig.url === undefinedfile:…objectstack.dbmongodbmongodb://localhost:27017/objectstackfile:…objectstack.dbtursofile:…objectstack.dbOnly the fallback rungs are refused on the standalone side (
unified-default/legacy-file). A URL from--database,OS_DATABASE_URL,DATABASE_URL,TURSO_DATABASE_URLor the project's declared default datasource is a statement about where the database is, and is honoured as before —file:DSN included.The pin this card exists for
packages/cli/src/utils/driver-vocabulary-parity.test.ts— 49 assertions that both hosts accept the same alias set for the same input, driving the real entry points. It lives in@objectstack/clibecause that is the only package that can import both@objectstack/runtimeand@objectstack/spec.The fork survived #3276, #5820 and #6265 — each of which shipped a green pin that drove exactly one host. That is why this file is the deliverable and not a detail.
Queue ejection — a poisoned oracle, not a vocabulary drift
This PR was ejected from the merge queue (build 31297062268) with four failures in
packages/cli/src/commands/database-driver-allowlist.pin.test.ts— #6860's pin, landed onmainby PR #6913 (9d425a94d) after this branch's base. Both PRs were green alone; they first met in the queue, because the queue runs the full suite while PR CI runs only the affected subset, and that file did not exist on this branch.The obvious hypothesis was wrong and was discarded by measurement. The natural read is "the canonical set moved (
mongo→mongodb, turso became builtin) while the hand-written oclif allowlist stayed on old spellings." It did not: #6860 already landed the canonical seven, includingmongodb,sqlite-wasmandmysql. Editing the allowlist would have been a no-op — andstart.ts/dev.tsare correspondingly untouched in this PR.The real defect is narrower and belongs to this PR. #6860's pin derives the driver-kind side by scanning
storage-driver.tswith a deliberately over-broad net and usingresolveStorageDefinitionas an oracle, readingerr.driverTypeout ofUnsupportedDriverErrorto decide what is real. This PR's new refusal of an explicitly-named unknown driver throws that same error type, carrying the operator's raw token indriverType— so every junk candidate now "resolved". The derived set went 7 → 13, gainingsafe,on-disconnect,factory,function,string,default,better-sqlite3. No allowlist could have equalled that set.Fix: give the error a discriminator, not the test an opinion.
UnsupportedDriverErrorgainsrecognized, defaultingtrueso pre-existing call sites (turso-with-no-URL) keep their meaning; the unknown-spelling throw passes{ recognized: false }; the pin returnsnullfor that case. The discriminator lives on the error rather than in the test, so the pin keeps asking the resolver what a token means instead of growing its own vocabulary.⛔ The assertion was not relaxed. Both sides are still derived and still required to be equal — the derivation was repaired. The pin's own docstring already defines the category ("a recognized kind that cannot become a definition still counts as recognized"); an unrecognized token was never in it.
Proof it was not silenced:
mongodbfromstart.ts's allowlistrecognized: falseLesson recorded for the repo: a derivation that reads a value out of an exception inherits every future meaning of that exception. #6860's pin was well built and its scope note explicitly anticipated this card's alias convergence without prejudging it — what it could not anticipate was the resolver growing a second refusal shape.
Reverse verification (predictions written before running)
mongorow still works"The reverse verification found a real hole in this PR's own pin. The parity test's REFUSE cases ran only
isDev: false, where the pre-#6345 CLI already returnednull; the silent-SQLite fallback only happens in dev. Reverting the CLI half left the pin green — onlystorage-driver.test.tscaught it. The pin now runs both modes and produces 6 reds under the same revert. Recorded because a pin that passes under the revert it exists to catch is worth more as a finding than as a green tick.Gates, on the re-merged tree
pnpm lint✅ · typechecks for spec / cli / runtime / service-datasource ✅ ·check:api-surface·check:generated·check:authorable-surface·check:liveness·check:strictness-ledger·check:spec-changes·check:upgrade-guide·check:spec-parsed-alias·check:adr-0087-registration(3 declared-breaking changesets, eachregistered datasource-driver-mongo-to-mongodb) ✅Full package suites (not filtered subsets — the filter is what let the first round's spec failure through): cli 101 files / 1095 tests, spec 350 files / 9094 tests, runtime standalone 87, service-datasource 264 — all green. The whole-monorepo
pnpm testwas not measured; the box is shared and returns pure timeouts under load. Reporting the per-package runs as the real signal rather than dressing an unmeasured suite as green.Regenerated baselines — the §11 trap fired and was caught
spec-changes.json/protocol-upgrade-guide.md: the repo's ownos-regenmerge guard flagged both as merged-without-a-text-merge; regenerated from the merged tree before committing, marker cleared. A subsequentrm -rf packages/spec/dist+ full rebuild + regeneration produced zero diff, confirming the artifacts were already correct.api-surface/contracts.json: an earlier post-mergegen:api-surfaceran against adist/built before the merge and silently dropped six exports main had added (StorageListPage,encodeStorageListCursor, …) — exactly AGENTS.md §11's trap. Caught by diffing the regenerated snapshot againstorigin/main's rather than trusting the generator's exit code. Final export-removal diff across everyapi-surface/*.jsonvsorigin/main: REMOVED: NONE.Changesets
Three
major, each carryingregistered datasource-driver-mongo-to-mongodb:BuiltinDriverIdloses a member; compile-time break even though runtime stays compatible via the alias.DRIVER_CATALOG.idvalue changes; failure mode is a silently-unmatched dropdown.Not touched, on purpose
packages/cli/src/commands/start.ts,dev.tsandcontent/docs/deployment/cli.mdx— #6860's surface; the oclifoptions:allowlist is untouched, and the queue investigation confirmed it needed no edit.content/docs/releases/— never touched (repo-wide prohibition).Follow-ups (filed here, not done)
--database-driveroclif allowlist from the shared table. Now that the single table exists,--database-driver mysql与--database-driver sqlite-wasm在 flag 解析阶段被拒 —— oclif 的 options 白名单漏了两个能用的驱动 #6860's hand-added entries can become a derivation — the "one vocabulary, one derivation" half of ruling item 3. Still open: the pin now enforces structurally what the derivation would enforce, and expanding a 41-file PR during a queue incident was the wrong trade.content/docs/data-modeling/drivers.mdx's alias table described the CLI only and is now true of both hosts; it still needsturso's contract and themongo→mongodbcanon.poolon a turso datasource is dropped in silence — pre-existing, unchanged by this PR.TursoDriverConfighas nomin/max, onlyconcurrency, and in local mode the driver is a better-sqlite3SqlDriver, which the engine'sPOOL_UNSUPPORTED_DRIVER_IDSrejects a pool block for. Making it a rejection is a new refusal on an authoring surface and needs its own ruling; recorded in the source comment.🤖 Generated with Claude Code
https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx
Generated by Claude Code