Skip to content

Every migration in packages/metadata/src/migrations/ requires driver.raw(...), a method no driver in this repo defines — the drivers expose execute(), so the documented cut-over call is a no-op that reports error #14023

Description

@zhuangjianguo

Found while implementing the timestamp-canonicalisation fix in migrate-sys-notification-to-event.ts (#13998). Filed separately rather than folded in: different defect class, and it needs its own decision about the driver surface these helpers accept. That card is not addressed by this one.

The mismatch

migrateSysNotificationToEvent guards, then drives, on driver.raw:

if (typeof driver?.raw !== 'function') {
    return { status: 'error', migrated: 0,
        error: 'migrateSysNotificationToEvent: driver must expose a .raw(sql, bindings?) method.' };
}

No data driver in this repo defines a raw method. Measured, with a firing positive control on the same expression:

$ grep -rnE '^\s*(public |protected |private )?(async )?raw\s*\(' packages/*/src packages/*/*/src --include=*.ts | grep -v '\.test\.ts'
packages/verify/src/harness.ts:84:  raw(path: string, init?: RequestInit): Promise<Response>;     # an HTTP harness, not a data driver

$ grep -rnE '^\s*(public |protected |private )?(async )?execute\s*\(' packages/drivers/*/src/*.ts | grep -v '\.test\.ts'
packages/drivers/driver-memory/src/memory-driver.ts:547
packages/drivers/driver-mongodb/src/mongodb-driver.ts:241
packages/drivers/driver-sql/src/sql-driver.ts:7956
packages/drivers/driver-turso/src/remote-transport.ts:1180

SqlDriver implements IDataDriver (sql-driver.ts:4081) and the contract itself declares execute?(command, options) (packages/spec/src/contracts/data-engine.ts:293) — there is no raw on it.

Why it matters

docs/handoff/adr-0030-notification-convergence.md gives operators this cut-over step, verbatim:

  1. Run migrateSysNotificationToEvent({ driver, data }) to carry existing notifications into sys_inbox_message + receipts.

An operator who passes their platform driver — the only driver that sentence can mean — gets { status: 'error', migrated: 0 } back and nothing happens. The migration is documented as the supported way to preserve users' existing bell notifications across the ADR-0030 cut-over, and it is published from @objectstack/metadata/migrations.

The failure is quiet in the shape that matters: status: 'error' is a returned value, not a throw, and the message names a driver method rather than saying the migration did not run.

Scope: the whole directory, not one file

All four members take driver.raw and none accepts execute:

  • migrate-sys-notification-to-event.ts — guard at :74, calls at :157, :174, :191, :203
  • migrate-env-id-to-project-id.ts — :90, :98 (doc at :39 states the requirement)
  • migrate-project-id-to-environment-id.ts — :170, :176 (doc at :110)
  • drop-projection-tables.ts — doc at :39 states the same requirement

The repo already has the correct shape, one package over

packages/metadata-protocol/src/migrations/ resolves both surfaces rather than assuming one:

// partial-index-probe.ts:94
if (typeof driver.raw === 'function') return (sql: string) => driver.raw(sql);
return (sql: string) => driver.execute(sql);

// seed-tenancy-backfill.ts:347
if (typeof driver.execute === 'function') {
  return { exec: (sql, params) => driver.execute(sql, params ?? []), client, ledger };
}
return { exec: (sql) => driver.raw(sql), client, ledger };

Adopting that resolver in the packages/metadata family is the obvious repair, but it is a decision about which surface these published helpers promise, so it is worth making deliberately rather than as a rider.

Why the tests do not catch it

Every case in migrate-sys-notification-to-event.test.ts builds its own double with a raw method — including the one that asserts the guard fires (errors cleanly when the driver has no raw()). The suite therefore pins the guard's message while never once exercising a driver the repo actually ships. The same is true of the sibling migrations' suites.

A repair should pin the real surface: drive at least one case through a driver type this repo defines, rather than through a double shaped to the helper's own assumption.

Re-run

grep -rn 'driver\.raw' packages/metadata/src/migrations/
grep -rnE '^\s*(public |protected |private )?(async )?raw\s*\(' packages/*/src packages/*/*/src --include=*.ts | grep -v '\.test\.ts'

Backlink: #13998 (the timestamp defect in the same file, where this was found). #13998 is not addressed here and stays open on its own terms.

Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. zhuangjianguo commented on Sep 1, 2026

    @zhuangjianguo
    CollaboratorAuthor

    Claim — domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L

    Branch: claude/issue-14023-migration-driver-exec-surface. Dispatching now. Clause-②: no — ⛔ 不预挂.

    Why no, stated so it can be overturned

    ⚠️ The tempting reading is yes: today these published helpers reject every driver this repo ships and afterwards they accept them, which sounds like widening the accepted input set of a published API.

    ⛔ I rule it no, on the same discriminator I applied to #13997 this round: Clause ② fires when the DECLARED contract moves. Here the declared contract is IDataDriver (packages/spec/src/contracts/data-engine.ts:293), which declares execute?(command, options) and has never declared raw. ⇒ The guard has been enforcing a surface the contract never declared. Bringing enforcement back to the declaration is the ordinary declared-≠-enforced repair, not a contract increment.

    ⛔ Tripwire — if the delivery needs to touch packages/spec/src/** at all (most plausibly to add raw? to IDataDriver so both surfaces are declared), that is the path limb and it fires unconditionally ⇒ STOP, re-declare Clause-②: yes, and report the fork. ⛔ Do not decide it in-flight.

    The ruling: ⛔ this is NOT a decision, it is copying a landed precedent

    The card says it "needs its own decision about the driver surface these helpers accept". Triage overruled that and I affirm: the answer is landed one package over, twice —

    // packages/metadata-protocol/src/migrations/partial-index-probe.ts:94-95
    if (typeof driver.raw === 'function') return (sql: string) => driver.raw(sql);
    return (sql: string) => driver.execute(sql);
    
    // packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts:347-349
    if (typeof driver.execute === 'function') { … }
    return { exec: (sql) => driver.raw(sql), … };

    ⇒ 2026-08-07 元判据: one operation, two implementations, inconsistent behaviour ⇒ 以受声明约束的一侧为准. The declared side is IDataDriver.execute. packages/metadata binds back to it.

    ⚠️ The two precedents try the surfaces in OPPOSITE order. ⛔ Do not copy one and stop. Pick one direction, apply it consistently across all four files, and write the reason in the comment. Since the real drivers only have execute and the contract only declares execute, try execute first is the honest order.

    ⭐ The part of this card that must reach the PR is the test finding, not the fix

    Every case … builds its own double with a raw method — including the one that asserts the guard fires. The suite pins the guard's message while never once exercising a driver the repo actually ships.

    ⇒ A test double shaped to the helper's own assumption locked the defect in. ⛔ Swapping raw→execute in the helpers and in the doubles moves the hole, it does not close it. The delivery must drive at least one case through a driver type this repo actually defines (MemoryDriver is the obvious one — it has execute at memory-driver.ts:547 and needs no server).

    Scope

    Four files, 12 call sites (migrate-sys-notification-to-event.ts · migrate-env-id-to-project-id.ts · migrate-project-id-to-environment-id.ts · drop-projection-tables.ts), plus the doc lines at :39 / :110 that state the raw requirement.
    ⛔ Do not touch #13998's timestamp defect in the same file — it is delivered on PR #14024 and is a different class. ⛔ Do not write a data backfill — #13998's data half is a maintainer floor and is open.


    Generated by Claude Code

  3. claude commented on Sep 1, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14023,
      "status": "done",
      "branch": "claude/issue-14023-migration-driver-exec-surface",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14084",
      "premise_still_valid": true,
      "summary": "All four helpers in packages/metadata/src/migrations/ now resolve their raw-SQL entry point through one shared resolver (src/migrations/driver-exec.ts): try execute, fall back to raw, refuse only when neither is present. execute goes first because IDataDriver declares it NON-optionally with bindings as the second positional argument. MATERIAL CORRECTION: the card, the triage note and the claim comment all cite data-engine.ts:293 as the declaration; that line is IDataEngine.execute?(command, options?: Record) -- a different member on a different interface whose second parameter is an options bag (implemented that way by ObjectQL.execute, called that way by service-analytics). The migrations take an IDataDriver, so packages/spec/src/contracts/data-driver.ts:108 governs, and it declares execute(command, parameters?: unknown[], options?) REQUIRED. This strengthens the ruling: execute is not merely the surface shipped drivers happen to have, it is the only one the contract guarantees. packages/spec is NOT touched, so the path tripwire did not fire. A new real-driver suite drives all four migrations through a real SqliteWasmDriver on in-process SQLite and asserts the physical schema, and pins that the real driver has no raw and does have execute. Also repaired two database-loader.test.ts fixtures that bolted raw onto an IDataDriver mock through an 'as unknown as { raw: unknown }' cast; one of them was asserting an absence that a run issuing no statements satisfied, so it gained a non-vacuity assertion. PM half-state note: the card WAS assigned on arrival (zhuangjianguo) and the claim comment was present, so no half-state to report. One cosmetic blemish: GitHub appended its own copy of the session footer to the PR body, so it appears twice; not PATCHed, because a PATCH downgrades the session-URL form to bare.",
      "tests": "All readings at HEAD fef41187f (final commit) unless noted. Exit codes captured BEFORE any pipe.\n(1) BUILD FIRST: pnpm --filter '@objectstack/metadata^...' build -> os-verify-lock VERDICT command-exit 0; later the full repo, turbo run build --filter='./packages/*' --filter='./packages/*/*' -> 'Tasks: 70 successful, 70 total' in 5m26s, run so the two ratchets could be MEASURED instead of skipped.\n(2) TESTS: pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2 -> 'Test Files 39 passed (39) · Tests 639 passed (639)'. Migrations subdir alone: 3 files / 23 tests green. The new real-driver file alone: 7/7.\n(3) ABLATION on the COMMITTED implementation. Subject: driver-exec.ts's execute limb, replaced by 'if (false /* ABLATION-MARKER-14023 */) {'. PREDICTED DIRECTION BEFORE RUNNING: red, because every execute-only and real-driver case loses its entry point. OBSERVED: 'Tests 8 failed | 15 passed (23)'. Mutation proven ON DISK before running, both directions: removed literal count 1 -> 0, injected marker 0 -> 1, git hash-object 32aaee4cd1b7 -> e847020acf27 (differs from the HEAD blob). Restore ran under trap 'RESTORE_FN' EXIT INT TERM with an ABSOLUTE path pinned to HEAD (git checkout HEAD -- \"$REPO_ROOT/PATH\", never the bare form) and was proven AFTER: git diff HEAD empty, git status clean, worktree blob back to 32aaee4cd1b7 == the HEAD blob, marker grep 0, execute limb grep 1. REBUILD LEG: NONE APPLIES, stated explicitly rather than fabricated -- the subject is reached from the tests through same-package RELATIVE imports, which vitest resolves to source, so it crosses no package wall through dist. The one cross-wall import in the new file (@objectstack/driver-sqlite-wasm) is not the subject and is unchanged by the mutation. Corroboration that the mutation reached executed code: the ablation went RED, which a dist-stale ablation cannot do (it stays green).\n(4) GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, RE-DERIVED after the final commit (the change set grew from 9 to 11 paths across the run; the scripts/ ledger edit pulled the family count from 15 to 28). All 28 run, ALL EXIT 0 at fef41187f. Two did real work rather than passing on arrival:\n  · check:engine-double-contract -> initially EXIT=1, its own verdict line 'check-engine-double-contract: 3 problem(s)', refusing the new file's engine double for not routing through the producers' dispatch predicates. Pinned it the way the sibling suite is pinned (assertEngineDeleteDispatch / assertEngineUpdateDispatch / assertEngineFindOnePredicate from @objectstack/metadata-core, NOT @objectstack/objectql -- that edge is a cycle turbo rejects). Then --write recorded three new pinned rows: ADD-ONLY, 15 insertions, no row deleted. Now EXIT=0, verdict line '674 (file, verb) row(s) held by the RETAINED ledger'.\n  · check:type-check-debt -> initially EXIT=1, verdict line '@objectstack/metadata: DEBT records 89 raw tsc error(s), `tsc --noEmit` now reports 91 (+2)'. Both errors were MINE: a wrong '([sql]: [unknown])' destructuring annotation against mock.calls, which is any[][]. Removed the annotations; re-measured 89 == the recorded number, ledger untouched. Now EXIT=0, verdict line 'check-type-check-coverage --re-measure: OK — 28 ledger entr(ies) re-measured in 364.6s, 1468 raw tsc error(s) total, none above its recorded number'. NOT-MEASURED check done explicitly: tsc --noEmit --listFiles confirms all five files I added or edited are inside that tsc program (1 hit each), so the 89 is a reading ABOUT them.\n  · Both gates first returned EXIT=3 (PREREQUISITE NOT MET = NOT MEASURED, never read as a pass) until the full build above.\n(5) MERGE: origin/main moved mid-flight and #13998's timestamp fix landed in migrate-sys-notification-to-event.ts. Merged base INTO head (never rebased, never force-pushed). One content conflict, resolved by keeping their canonicalTimestampText block WHOLE and re-applying only my selectLegacyRows signature on top. scripts/engine-double-contract.pinned.json auto-merged and was verified BY CONTENT, not by exit code: 671 base rows + 3 mine + 3 theirs = 677 in the merged tree, 'lost rows: NONE'. The auto-merged test files were diffed against origin/main by case name; the only case absent is the one I deliberately renamed. Rebuilt and re-ran everything on the merged head.\n(6) REPO-WIDE SCANS: no narrowing was needed in the end -- the full build made both repo-wide ratchets genuinely measurable, so nothing here is a declared narrowing. pnpm lint (eslint over the whole repo) was NOT run; it is CI's run and was not in the derived family list.",
      "mcp_calls": "0 — the REST channel was reachable from this seat (repo probe HTTP 200, /rate_limit core 15000/hr), so every GitHub read and write went through repo-scoped REST: issue + comments read, dedup listing, two issue creations, PR creation, and the PR/issue read-backs. No MCP GitHub tool was called at any point.",
      "pm_assumptions": {
        "A2.1": "FALSIFIED IN THE COUNT, CONFIRMED IN THE SHAPE. The population is 4 files, but NOT 12 call sites -- the card's grep 'driver\\.raw' misses three real invocations spelled 'driverAny.raw(...)' (drop-projection-tables.ts:51, migrate-env-id-to-project-id.ts:72, migrate-project-id-to-environment-id.ts:151). Expression run: grep -rn 'driver\\.raw\\|driver?\\.raw' packages/metadata/src/migrations/ -> 12 hits, of which only 8 are runtime calls; the other 4 are 3 JSDoc lines plus 1 typeof guard. Widening to '\\.raw(' finds the 3 driverAny sites. TRUE TALLY: 11 runtime raw() invocations + 4 guards (3 typeof + the sys-notification one) + 3 JSDoc lines + 2 error-message sentences. All 11 invocations and all 4 guards are converted. OUTSIDE the four files: 'driver.raw' users are metadata-protocol/src/migrations/partial-index-probe.ts:94, seed-tenancy-backfill.ts:354 and protocol.ts:5159 -- all three ALREADY resolve both surfaces, so none has this bug; their order divergence is filed as #14083 and NOT folded in. Everything else matching '.raw(' is knex.raw / stack.raw / an HTTP harness, i.e. a different member. Positive control: the same grep DOES return the four target files.",
        "A2.2": "ATTACKED, AND THE ASSUMPTION SURVIVES ON A DIFFERENT FOOTING THAN THE ONE YOU STATED. Measured all four shipped execute() signatures: SqlDriver 'execute(command: any, params?: any[], options?: DriverOptions)' -- real, passes params to knex.raw(command, params || []); Turso remote-transport 'execute(command: unknown, params?: unknown[])' -- real, builds {sql, args} when params are non-empty; MemoryDriver 'execute(command: any, params?: any[])' -- logs a warning and returns null, params ignored; MongoDbDriver 'execute(command: unknown, _parameters?: unknown[], options?)' -- underscore-prefixed, never read, and a string command is returned verbatim. So bindings ARE carried positionally on both drivers that can run SQL, and the mechanical swap is correct FOR THEM. Your worry was well-placed but pointed at the wrong contract: IDataEngine.execute?(command, options?: Record) really does take an options bag second -- and that is a DIFFERENT member on a DIFFERENT interface, implemented that way by ObjectQL.execute (engine.ts:12800) and called that way by service-analytics (engine.execute(knexSql, { args: params })). IDataDriver.execute declares 'parameters?: unknown[]' positionally and REQUIRED. Since these helpers take an IDataDriver, no fork exists and no spec edit is needed. The residual risk is the two drivers that accept and no-op: they satisfy the typeof probe, so the resolver selects them and they then answer every column probe with 'absent'. That is filed as #14082 and named in driver-exec.ts's header rather than papered over -- DriverCapabilities has no member that separates 'implements the escape hatch' from 'can run SQL', so telling them apart is a contract question.",
        "A2.3": "YES -- two places, both preserved and both re-pointed rather than deleted. (a) migrate-sys-notification-to-event.test.ts asserted status 'error' and error containing '.raw' for driver {}. (b) migrate-project-id-to-environment-id.test.ts's #13219 case asserts the THROWN message states its remedy exactly once (counted via a /g regex), that no sentence runs into the next (/[a-z]\\.[A-Z]/ must not match), and that a conforming driver is still named (/SqlDriver/). All three properties are unchanged and still asserting; only the sentence they are counted over moved, so the regex was re-pointed to the new wording. THE TOTALITY FLOOR HOLDS: a driver with NEITHER surface is still refused, by both the returning helper and the throwing ones, and both cases were renamed to say so ('...when the driver has NEITHER raw() nor execute()'). Added the missing other half in both files: a driver offering ONLY execute is accepted and its bindings arrive as the array. A third dependant surfaced that the card did not name: database-loader.test.ts had two cases bolting raw onto an IDataDriver mock via 'as unknown as { raw: unknown }'; the full package run caught one of them going red, and the OTHER was passing VACUOUSLY (it asserts no statement matched a pattern, which a run issuing no statements satisfies). Both re-pointed to the mock's own execute, and the vacuous one gained a 'nothing ran' guard first.",
        "A2.4": "Three JSDoc lines were false and are corrected: drop-projection-tables.ts:39, migrate-env-id-to-project-id.ts:39, migrate-project-id-to-environment-id.ts:110. Two error-message sentences were false and are corrected. docs/handoff/adr-0030-notification-convergence.md:101 needed NO edit and correcting it does NOT belong in this PR: it names the call without naming any driver surface, so it was false IN EFFECT (the step did nothing) and becomes true with this change -- there is no wrong sentence to fix. Swept for other false prose with grep -rniE 'raw\\(sql|\\.raw\\(\\)|expose a raw|raw query method|must expose' over docs/ packages/metadata/ content/ (--md/--mdx, CHANGELOG excluded): ZERO hits outside the source files above. Positive control on the same sweep: the unrestricted grep over .ts DOES return the guard sentences. content/docs/releases/ is untouched, as always."
      },
      "clause_2": "no",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #14082: driver-memory / driver-mongodb execute() answer without running the command and without refusing -- MemoryDriver returns null for everything, MongoDbDriver hands a string command back verbatim and never reads _parameters. Both satisfy the NON-optional IDataDriver.execute declaration, so a typeof probe selects them; measured consequence, dropProjectionTables(memoryDriver) reports 'dropped' for all five tables. Labelled domain:engine + pm:on-hold per #5499 (maintainer ruling 2026-08-05 freezing driver-memory/driver-mongodb investment, whose standing triage rule says exactly this). I did NOT measure it against #5499's escalation exception (a driver defect that makes CI falsely green/red) and said so in the card. Sibling of #13878, not a duplicate.",
        "filed as #14083: [finding] metadata-protocol's three raw-SQL resolvers try raw and execute in two different orders (partial-index-probe.ts raw-first, seed-tenancy-backfill.ts execute-first, protocol.ts ensureOverlayIndex raw-first), and the raw limb is dead on every driver this repo ships. The residue of the same 2026-08-07 meta-criterion this card applied, in the package the card's scope excluded. Labelled finding + domain:engine, observational, no pm:queue.",
        "NOT filed, recorded as a measured NON-finding: IDataEngine.execute?(command, options?: Record) diverging from IDataDriver.execute(command, parameters?, options?) is NOT a defect. Checked before filing -- ObjectQL.execute (packages/objectql/src/engine.ts:12800) matches the engine declaration exactly, and its one real caller (service-analytics/src/plugin.ts:403) passes an options bag with args inside. Two different surfaces on two different interfaces, each self-consistent. This was A2.2's biggest worry and it dissolved on measurement."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  4. claude commented on Sep 1, 2026

    @claude
    Contributor

    PM 复核 —— PR #14084。⭐ 席位把这张卡从「运维手册上的一条死路」变成了「每次启动都在静默失败」

    domain:engine 车道 PM,session session_01F3jdziLbAPGeceVNmSox5L。

    ⭐⭐ 最重要的发现:它不是只影响照手册操作的人

    卡与分诊都读作「一条已发布、写进运维手册的迁移,对任何真驱动都不工作」。席位测出它还有一个启动路径调用方,我独立复验属实:

    packages/metadata/src/loaders/database-loader.ts:411
        await migrateProjectIdToEnvironmentId(driver).catch(() => undefined);
                                                      ^^^^^^^^^^^^^^^^^^^^^ 吞掉
    packages/metadata/src/loaders/database-loader.ts:481
        await migrateProjectIdToEnvironmentId(this.driver!);
    

    ⇒ v5.0 的 project_id → environment_id 前向迁移,在真实驱动上、在启动路径上、每次都抛错并被吞掉。
    ⚠️ 而 :424 给运维的补救建议是「用 migrateProjectIdToEnvironmentId(driver) 显式重跑」—— 那条建议同样跑不通。

    ⇒ ⛔ 这证伪了「仓内零调用方」这条读数。它对 migrateSysNotificationToEvent 成立(#13998 的席位没测错),但对整个目录不成立。

    ⭐ 契约更正:三方都引错了文件,而更正让裁定更硬

    卡、分诊、以及我自己的认领评论都引 packages/spec/src/contracts/data-engine.ts:293 的 execute?(command, options)。那是另一个接口上的另一个成员(IDataEngine,第二参是 options 包,ObjectQL.execute 就是那样实现、service-analytics 就是那样调用的)。这些迁移收的是 IDataDriver,管辖的是:

    packages/spec/src/contracts/data-driver.ts:108
      execute(command: unknown, parameters?: unknown[], options?: DriverOptions): Promise<unknown>;
    

    非可选,绑定参数在第二个位置 —— 与 raw(sql, bindings?) 的调用形状逐位对齐。
    ⇒ ⭐ execute 优先不再只是「更诚实的顺序」,它是契约唯一保证存在的那一个。我已独立复验,采信。

    ⭐⭐ A2.2 被认真攻击了,而且攻出一个真问题

    我担心「execute() 的 bindings 语义未必在四个驱动上一致」。席位量了全部四个:

    驱动 行为
    SqlDriver 真执行,knex.raw(command, params || []) ✓
    Turso remote-transport 真执行,非空参数时构造 {sql, args} ✓
    ⚠️ MemoryDriver 打条警告就返回 null,参数不读
    ⚠️ MongoDbDriver 字符串命令原样返回,_parameters 带下划线、从不读

    ⇒ 后两者满足非可选的 execute 声明,因而通过 typeof 探测,然后对每个列探测答「不存在」。
    ⭐ 实测后果:dropProjectionTables(memoryDriver) 对五张表全部报告「已删除」——一条语句都没执行。

    ⇒ 席位把它立成 #14082,并写进 driver-exec.ts 的头注而不是糊过去。⭐ 且它点出了根因:DriverCapabilities 没有任何成员能区分「实现了这个逃生舱口」与「真能跑 SQL」。
    ⚠️ 它按 #5499 的维护者裁定(2026-08-05 冻结 driver-memory/driver-mongodb 投入)挂了 pm:on-hold,并明说自己没有对照 #5499 的升级例外去测。⛔ 该克制是对的。

    ⛔ A2.1:卡的站点计数被证伪

    卡的 grep driver\.raw 漏掉三处真实调用,它们拼作 driverAny.raw(...)(drop-projection-tables.ts:51、migrate-env-id-to-project-id.ts:72、migrate-project-id-to-environment-id.ts:151)。
    真实盘点:11 处运行时调用 + 4 个守卫 + 3 行 JSDoc + 2 句错误文案——卡说的「12 处」里只有 8 处是运行时调用。全部 11 处与 4 个守卫已转换。带正对照(同一 grep 确实返回那四个目标文件)。

    ⭐ A2.3:抓到一个空泛通过的测试

    除卡点名的两处依赖外,席位找到第三处:database-loader.test.ts 有两个用例用 as unknown as { raw: unknown } 把 raw 焊到 IDataDriver mock 上。整包跑时一个跑红了,⭐ 另一个是空泛通过的 —— 它断言的「缺席」被一个「什么都没执行」的运行同样满足。⇒ 修好之前先给它补了非空泛断言。

    总体性地板保住了:两个表面都没有的驱动仍被拒绝(返回式与抛出式两种都是),且两个用例都改名说明了这一点;并补上了卡没要求的另一半——只有 execute 的驱动被接受,且绑定参数以数组形式抵达。

    门:两个真做了工,且都没动台账

    • check:engine-double-contract 初 EXIT=1(新文件的 engine double 未走生产者的 dispatch 谓词)⇒ 按姊妹套件的方式钉住(⭐ 用 @objectstack/metadata-core 而非 @objectstack/objectql——后者是 turbo 拒绝的循环边),--write 记录 3 行 ADD-ONLY,⛔ 无删除。
    • check:type-check-debt 初 EXIT=1,+2 个错误 —— ⭐ 两个都是席位自己的(对 mock.calls 写了个错的解构标注,而它是 any[][])⇒ 去掉标注,重测 89 == 台账记录值,⛔ 台账未动。
    • ⭐ 并且显式做了 NOT-MEASURED 检查:tsc --noEmit --listFiles 确认新增/编辑的五个文件都在该 tsc 程序内(各命中 1 次)⇒ 那个 89 是关于它们的读数。
    • 两道门在全量构建前都是 EXIT=3,⛔ 记为 NOT MEASURED,从不当作通过。

    ⭐ 合并轮:正是 #14064 要的那种纪律

    main 中途移动,#13998 的时间戳修复落进了同一个文件。席位把 base 合进 head(⛔ 未 rebase、未 force-push),冲突处完整保留对方的 canonicalTimestampText 块、只把自己的 selectLegacyRows 签名重新叠上。
    ⭐ 而 scripts/engine-double-contract.pinned.json 是自动合并的,它按内容验证而不是看退出码:671 基线 + 3 我的 + 3 他们的 = 677,"lost rows: NONE"。⇒ 这正是 #14064 那条教训的正确应用。

    另一条:一个先查证再决定不立卡的非发现

    IDataEngine.execute?(command, options) 与 IDataDriver.execute(command, parameters?, options?) 的差异不是缺陷——两个接口上的两个面,各自自洽(ObjectQL.execute 与 service-analytics 的调用完全匹配引擎侧声明)。⇒ 席位原话:「这是 A2.2 最大的担忧,它在测量中溶解了。」 ⭐ 查证后不立卡,与查证后立卡同样有价值。

    另立 #14083:metadata-protocol 的三个 resolver 以两种相反顺序尝试 raw/execute,且 raw 支在本仓出货的每个驱动上都是死代码 —— 同一条 2026-08-07 元判据的残留,落在本卡范围之外的那个包里。

    定级:⚠️ 我倾向上调,但不在本轮改

    启动路径静默失败把影响面从「照手册操作的运维」扩大到「每一次启动」。⛔ 但我不据此改级:影响取决于有多少部署实际处在需要该迁移的状态,而那个读数没有人有(#13998 已经测出 sys_migration 台账里根本没有注册这条迁移)。⇒ 记在这里,与 #13998 的数据半边一并交维护者。

    状态

    ⛔ 尚未放行 —— PR #14084 全绿(inc=0 / fail=0 / Governed Surface Queue Guard success)、对 main dda969cd71 clean 且对照发火,但放行动作(翻 draft、挂 auto-merge)在本会话只有 GraphQL 通道而它被门控,MCP 通道额度耗尽。已如实上报维护者。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions