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 |
…录纳入门 (objectstack-ai#6530) (objectstack-ai#7302) 按 maintainer 2026-08-08 的 Option C 裁决实施。 原来的 `(N schemas)` 是**自指**的:N 只与它自己下面那张表比对,与 content/docs/references/{category}/ 的真实面之间没有任何门在看。objectstack-ai#6530 实测出 `## Data Protocol (17 schemas)` 之上是一个发布 29(今日 30)个 schema 页的目录, 其中 13 个页在整张页面上没有任何一行 —— 而门一直是绿的,并且永远会是绿的。 13 个标题一律改为 `(N of M schemas)`:N 仍是本表行数,M 是该分类目录实际发布的 参考页数(`.mdx` 去掉 index.mdx)。表继续保持策展子集 —— 一个把 30 个 data 页 全镜像过来的 quick reference 就不 quick 了 —— 但措辞不再读作总数,且 N < M 这个 正常状态从此有门在看。 今日 main 上全部 13 个 M 重新实测(未复用 08-08 的表):只有 data 动了,29 → 30, 因为 e2798fa(objectstack-ai#6345 / PR objectstack-ai#6910)在 08-09 新增了 driver-turso.mdx。这正是这道门 值得有的证据 —— 两天,一格漂移,原来的自指门看不见。 跨切规则(裁决要求随行): - 每一行都必须有链接。Shared 的 `Connector Auth` 原来是**裸名无链接**,现在指向 /docs/references/integration/connector —— 那个文件在 src/shared/,但 `@objectstack/spec/shared` 并不发布它,它经 `@objectstack/spec/integration` 到达消费者,build-docs.ts 的 `integration:` 注释与 scripts/lib/root-index.ts 都写明了这一点(后者还把 `shared/connector-auth.zod.ts` 这类行点名为缺陷类)。 Key Schemas 一并从未发布的 ConnectorAuthConfig 改为已发布的 ConnectorInstanceAuth。 - 三行指向 references/ 之外的行统一带 `↗` 标记(UI 的 Widget Contract、Kernel 的 Events、Shared 的 Connector Auth)。标记就是 N 与 M 的对账口:带标记的行算进 N, 但不是 M 里的那些页。门双向校验 —— 未标记的行必须指向本小节自己的分类目录下 一个真实存在、且不与别行重复的页;指向别处的行必须带标记;带标记却指回自己 目录的行也红,否则标记会悄悄从覆盖率里减掉一页。 - 分类级策展写在页面上而不是留作默认:新增 `## Categories Without a Section` 一节,写明 references/studio/(3 页)与 references/contracts/(0 页,只剩一个 meta.json)为何没有小节;门同样读这张表,任何分类目录必须要么有小节、要么在 这里被声明,不能两者都是、也不能两者都不是。 门的自测从 9 例扩到 22 例。第 3 例是反空转的那一例:页面内部自洽、只有目录不符, 删掉 M 断言后它实测归零(4 条断言全红),即 objectstack-ai#6530 记录的"永远绿"状态。第 18/20 例 对分类级覆盖扫描做同样的反向验证。 Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn Co-authored-by: Claude <noreply@anthropic.com>
…air PR-NUMBER` is the only spelling (objectstack-ai#18250) Fixes objectstack-ai#18181 ## What changed Three lines across two governed rules-layer files; line counts unchanged (os-dev.md 403/403, SKILL.md 812/812), every edited line at or under the 120-byte cap, no re-wrap anywhere, and the decision-frame block SKILL.md :734–:755 untouched (md5 `3327d02c56f8a0eca88569dad2270f32` before and after). **`.claude/agents/os-dev.md` :287** — the line the card names, re-derived on `b3b43b6` (the card body's `:288` had drifted; the filer's correction comment 5664488072 gives :287): ```diff -- 认领写 `Clause-②: yes` ⇒ 开 PR 同笔挂 `needs:contract-review`,报告附 `--pair N` 退出码。 +- `needs:contract-review` 归席位,⛔ 不挂不摘不等;报 PR 上有无与 `--pair PR-NUMBER` 退出码作读数。 ``` The dev no longer hangs the carrier. It never hangs, removes or waits for `needs:contract-review`; it reports whether the label is present on the PR and the `--pair PR-NUMBER` exit code — as a reading, not as a gate it must clear. **`.claude/agents/os-dev.md` :301** — the neighbouring `skip-changeset` label-write rule (mechanism assumption A2, same file surface, same class — an additive label write the container may refuse): ```diff -- 被拒 ⇒ 停下报 `blocked` 点名端点与状态码,⛔ 不换 MCP 写道;写后必做对比式读回。 +- 被拒 ⇒ 报端点与状态码、席位代挂,⛔ 不报 `blocked`、不走 MCP;写后必做对比式读回。 ``` One rule now covers both carriers: a refused additive label write is reported with endpoint and status and the seat hangs the label; the dev never reports `blocked` for it and never switches to an MCP write. **`.claude/skills/pm-dispatch/SKILL.md` :646** — the one `--pair N` token (REGION claim: this line only): ```diff -- `Clause-②: yes` 认领同笔在卡上挂标;PR 开出即读 `check-clause2-carriers --pair N` 为 0 再请审。 +- `Clause-②: yes` 认领同笔卡上挂标;PR 开出读 `check-clause2-carriers --pair PR-NUMBER` 为 0 再请审。 ``` `--pair PR-NUMBER` is now the only spelling in the three governed files (contract-review.md :42 already spelled it so). ## Why Direction A of the card's grading (comment 5673619664), executed as ruled: the `needs:contract-review` carrier is the PM seat's to hang — on the card at the claim and on the PR when the PR appears — which is what every seat already does; the dev never blocks on a label write; `--pair PR-NUMBER` becomes the only spelling. Option B (granting the dev container the label write) is a permissions widening and was ruled not this card. The two defects the card and its correction measured: a dev that obeyed :287 literally was refused the additive label write by its container's write classifier on two independent objectui runs, while an objectstack dev could write it — per container, unknowable in advance; and `--pair N` bound `N` to the nearest referent, the card, while the script takes a PR number and answers a card number with exit 2 (UNJUDGED), which a naive capture reads as a red gate. ## Mechanism readings - **A1** — :287 is the line on `b3b43b6` (108 B; the card's :288 is the `skills/**` line). Replaced as above. Under the byte cap the `Clause-②: yes` trigger did not fit, so the rule is now unconditional: on every PR the dev hangs nothing, removes nothing, waits for nothing, and reports carrier presence plus the `--pair PR-NUMBER` exit code. That superset is deliberate — `--pair` also carries the widening-tell row, which only fires on a `Clause-②: no` claim, so a reading on every PR is worth more than one on `yes` claims alone. - **A2** — measured on the text: :301's `被拒 ⇒ 停下报 blocked` is the same container-refusal class one line over, and nothing about `skip-changeset` requires the dev to block on it: CI's changeset gate reads the label, the dev's report says whether it landed, and the seat can hang it on that report. So one rule covers both carriers and :301 changed as above; :299 (the label is your step, hang it as soon as the PR opens) and :300 (additive endpoint) stay — the dev still attempts the write. The live half of the measurement is this PR's own `skip-changeset` write; its outcome (landed, or the endpoint and status of the refusal) is in the `os-dev-report` comment on the card, not here, because the PR body is written before the label write and is not re-sent. - **A3** — `git grep -n -- '--pair N\b' -- '.claude/**' 'scripts/pm/**'`: before 6 hits (os-dev.md :287, SKILL.md :646, and 4 in `scripts/pm/**`); after 4 hits, all outside the claimed surface: `check-clause2-carriers.mjs` :24 and :490 (docblocks), :4947 (a self-test title), and `check-widening-tells.mjs` :492 (docblock). In the three governed files the count is 0. Lit control `--pair PR-NUMBER`: 1 before (contract-review.md :42), 3 after (:42, os-dev.md :287, SKILL.md :646). The script docblocks bind `N` to a PR number in their own text (:11 spells `--pair 13910 # ONE PR`), so the ambiguity is not live there; noted below, not edited. - **A4** — measured from the sibling checkout on this container: `cd /home/user/objectui && PM_SWEEP_REPO=objectstack-ai/objectui node ../objectstack/scripts/pm/check-clause2-carriers.mjs --pair 9552` exits 0 with the provenance line `every row below is read from objectstack-ai/objectui (source: PM_SWEEP_REPO)` and the verdict `PR objectstack-ai#9552 / card objectstack-ai#6910 — the clause-② declaration is readable in the fixed spelling and both carriers agree` (token present, 3 reads served; the proxy re-exec happened by itself). A control on objectui#9495 (no longer open) exits 2 — UNJUDGED, with the board still retargeted. So the invocation works from a sibling checkout. The byte cap leaves no room on :287 for the invocation (the line stands at 119 B), so the line carries only the flag spelling; the script header (:20–:24) already spells the sibling invocation and every run prints the board it read, which is the remedy text the ruling asks to keep in the script. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; change set from the merge base): 18 families on `a2cbbef0`, re-derived on the merged head `0c934967` (merge base `68fea8bc`) — identical list. All 18 run in the foreground on `0c934967`, exit codes captured by redirect before any pipe, all 0: `check-closing-keyword-parity` (+ `--self-test`) · `check-comment-mask-corpus` · `check-governed-queue-guard --self-test` · `check:doc-formula-expressions` (lint pkg) · `check:agent-model-declared` · `check:agent-test-spelling` · `check:commit-card-trailers` · `check:doc-authoring` · `check:driver-memory-census` · `check:nul-bytes` · `check:pm-governed-merges` · `check:pm-governed-prose` · `check:pm-skill-id-lint` · `check:pm-skill-ratchet` · `check:refd-timer-probe` · `check:skill-frame-sync` · `check:watch-hint-literal` Reconciliation on `0c934967`: `✓ dispatch-gates --ran: 18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of them is 3)`. Verdict lines: `✓ check-skill-line-ratchet: .claude/agents/os-dev.md is 403 lines (ceiling 403; headroom 0)` · `✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812; headroom 0)` · `✓ check-agent-model-declared: 1 agent definition(s) under .claude/agents/ all declare a model` · `✓ check-skill-id-lint: 27 file(s) clean` · `✓ check-skill-frame-sync: the one declared copy of the decision frame is internally coherent`. `check:doc-formula-expressions` first answered exit 3 (PREREQUISITE NOT MET — `@objectstack/formula` and `@objectstack/lint` unbuilt in a fresh worktree); prerequisite built under the verify lock (`turbo run build --filter=@objectstack/formula --filter=@objectstack/lint`, VERDICT command-exit 0), rebuilt again after the merge, then the gate exits 0. The exit 3 is not a measurement and is not counted as one. Not run locally, by design: the 11 wide-population families and the whole-repo `pnpm lint` sweep belong to CI. Control-byte self-scan on both edited files: no hits. ## Line budget | file | before | after | ceiling | edited lines (bytes) | | --- | --- | --- | --- | --- | | `.claude/agents/os-dev.md` | 403 | 403 | 403 | :287 108→119 · :301 110→114 | | `.claude/skills/pm-dispatch/SKILL.md` | 812 | 812 | 812 | :646 116→118 | Paid by density in place; no line merged or split. ## Deviations, declared 1. SKILL.md :646: the token swap alone (`N` → `PR-NUMBER`, +8 B) puts the line at 124 B, over the 120-byte cap. Two characters were dropped on the same line (`在`, `即`), content unchanged, landing at 118 B. Still inside the one-line REGION claim. 2. os-dev.md :287: the `Clause-②: yes` trigger is gone for the byte cap; the rule is unconditional (see A1). If the seat wants the trigger back, the price is the `作读数` tail or the `不等` limb. 3. os-dev.md :287 does not spell the objectui invocation (A4); the line names the flag, the script header names the sibling form. 4. os-dev.md :301 edited under A2 (inside the file surface and the card's class). objectui's copy and the dispatch-order template are untouched. 5. `skip-changeset`: nothing published moves — `.claude/**` is on the fast path (no package `files[]` ships it). 6. `origin/main` merged at `68fea8bc` (one spec commit, nothing on this surface) before opening; objectstack-ai#18242 remains open on SKILL.md :172–:185 — disjoint from :646. ## Acceptance notes - **To file (class b, for the seat — dedupe words: `needs:contract-review`, `PR 创建者`, `同笔挂`, SKILL.md, direction A):** SKILL.md :645 still reads 「`needs:contract-review`(恒英文)由 PR 创建者随可复审契约增量同笔挂:draft PR,或先到的报告。」 For a dev-created PR the creator is the dev, which direction A says never hangs it. Outside this card's REGION claim (:646 only), so not edited here; it sits on the SKILL.md chain the seat owns. - noted, not filed: 4 `--pair N` spellings remain in `scripts/pm/**` docblocks and one self-test title (listed under A3); each is next to text that binds N to a PR number. 承接者:无. - noted, not filed: os-dev.md :300's 「可达性按会话探,先探后用」 is redundant with the new :301 — the write attempt is the probe. Left as is; deleting it frees bytes, not a line. 承接者:无. - Observed on this PR after opening: the labels present are reported in the `os-dev-report` comment with a read-back; under one shared identity the author field says nothing about who hung what. ## 维护者速读(草稿) **改了什么**:改了两个受管规则文件里的三行,行数不变。开发 agent 定义(os-dev.md)一行:`needs:contract-review` 这个标签归席位挂,开发 agent 不挂、不摘、不等,只在报告里写 PR 上有没有它、以及 `--pair PR-NUMBER` 的退出码,作读数。另一行:加法写标签被容器拒绝时,报端点与状态码、由席位代挂,不再报 `blocked`、不走 MCP。PM 技能(SKILL.md)一个 token:`--pair N` 改成 `--pair PR-NUMBER`。 **为什么改**:两次独立实测里,开发 agent 的容器拒绝写标签,而规则要求它开 PR 同笔挂——照做就失败、如实报告又像抗命。分诊裁定 A 向:载体由席位挂(认领时挂卡、PR 出现时挂 PR),开发 agent 只报所见。同一行的 `N` 让人绑到卡号,脚本要的是 PR 号,给卡号返回 exit 2(未判),会被当红灯读;`PR-NUMBER` 是唯一拼写。 **风险与代价(含回滚)**:行数与上限不变,每行 ≤120 字节,决策框架块未动。风险是开发 agent 不再自己挂载体,若席位漏挂,条款② PR 的载体会缺失——但席位侧已经是现行做法,且开发 agent 报告的 `--pair` 读数会暴露缺失。回滚 = revert 本 PR,无其他副作用。 **席位意见**: **你要做的**:审阅上面三行措辞(尤其 :287 去掉了 `Clause-②: yes` 触发词、改为对每个 PR 都成立);同意即给授权批准,由席位落地;不同意请指出要保留的措辞。 --- _Generated by [Claude Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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