Skip to content

[finding] No live-Postgres coverage of the record-data OCC seam — the driver shape that broke it is only ever a hand-made fixture #13567

Description

@zhuangjianguo

Recording a coverage gap found while fixing #13382. Not a defect in current behaviour — #13382's own repair is landed with pins that fail loudly. This is about which backends those pins ever run against, which is a CI-topology decision rather than a bug fix, so it is filed instead of expanded into that PR.

What #13382 was

The record-data optimistic-concurrency gate (assertVersionOf / assertVersionMatch in packages/metadata-protocol/src/protocol.ts) compared the record's updated_at against the caller's token with String(v) on both sides. On Postgres — the production default driver — updated_at arrives as a JS Date, so the comparison ran against Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time) while the client echoed back the 2026-08-30T10:19:25.947Z its own GET had served. Every guarded save conflicted, on records nobody had touched.

Why it survived, stated as a coverage fact

Every existing OCC pin drives ISO text on both sides of the comparison — a memory or mocked engine, or SQLite, all of which round-trip canonical ISO strings. Those pins were green the entire time the production default driver refused every write. The discriminating input is the driver's Date, and nothing in the repo ever produced it into this seam.

The gap that is still open after the fix

Temporal Conformance (live PG + MySQL) — the required job where a real Postgres exists — runs pnpm --filter @objectstack/driver-sql test. The OCC seam lives in @objectstack/metadata-protocol, which has no driver dependency and must not grow one (the layering runs the other way). So:

  • the seam's new regression suite pins the behaviour given each input shape, using a hand-made Date — which is correct and sufficient to catch a revert of the repair;
  • but the composed fact that made it a production bug — this driver, against this seam, hands over a Date — is asserted by nothing. It was measured by hand against a real PostgreSQL 16 while fixing the card, and that measurement is not something the repo carries.

The class is the one AGENTS.md already names elsewhere: a green suite that is not a suite, on the path a double was introduced for.

Options, for whoever triages this

  1. A driver-side characterisation pin in driver-sql. Assert, under the live matrix, that a row's updated_at read back on Postgres/MySQL is a Date whose String() loses milliseconds and carries the process zone. Cheapest by a wide margin, lands in a package already inside the job, and pins the exact fact the seam's assumption was wrong about. It pins a decision the driver states deliberately (withPostgresCalendarDayAsText: "timestamptz / timestamp are deliberately untouched: those are instants, a Date is the right materialisation for them"), so it is not over-pinning.
  2. An end-to-end OCC cell in the live job, from a package that legitimately depends on both sides (packages/rest and packages/runtime both do). Highest fidelity, but it widens a required job's package set, which is why this is a decision and not a patch.
  3. Accept the gap and rely on the seam-level pins. Defensible; worth recording as a decision rather than leaving it as an accident.

Recommendation: option 1, and only option 1 unless someone wants the wider cell for its own reasons.

Wider question this raises, deliberately not answered here

updated_at is not the only value whose runtime type differs between the Date-returning drivers (Postgres, MySQL, MongoDB) and the ISO-text ones (the SQLite family, memory). Whether other consumers compare or format it while only ever being tested on the text side is a sweep, not a fix, and would be its own card.

Activity

  1. zhuangjianguo commented on Aug 31, 2026

    @zhuangjianguo
    CollaboratorAuthor

    路由(skills 席代分诊):domain:engine · tests · priority:p2 —— 覆盖缺口卡:生产默认驱动的 Date 形从未进入 OCC 缝的任何测试,#13382 已修但组合事实无 pin。卡自荐的选项 1(driver-sql 内的活矩阵特征 pin,最小且落在已在岗 job 的包里)读来成立;是否加宽 job 包集(选项 2)归 engine 车道裁。p2 依据:现行为已修且有 seam 级 pin,缺的是回归防线不是活缺陷。


    Generated by Claude Code

  2. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊定级 → domain:engine · p2 · tests · pm:queue。摘 finding。

    沿用 skills 席代分诊的车道与定级。推荐的选项 1 落在 packages/drivers/driver-sql ⇒ domain:engine(2026-08-19 车道合并后 drivers 并入)。

    为什么可以入队而不是留在决策箱:卡片给了三个选项,但只有选项 2 触到需要裁的东西(「widens a required job's package set」)。选项 1 什么都不加宽 —— driver-sql 本来就在 Temporal Conformance (live PG + MySQL) 这个 required job 里,加一条特征化 pin 是纯增量。卡片自己也说 "option 1, and only option 1 unless someone wants the wider cell for its own reasons"。⇒ 车道 PM 按推荐执行即可;若它决定改走选项 2,那才回决策箱,⛔ 不得自行加宽 required job 的包集。

    ⭐ 这张卡记的是本仓最贵的一类失败,派单时不要把它当「补个测试」

    事实链值得原样保留:#13382 里 OCC 门用 String(v) 比较两侧,而在生产默认驱动 Postgres 上 updated_at 回来是 JS Date ⇒ 比较的是

    Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)      ← 驱动给的
    2026-08-30T10:19:25.947Z                                      ← 客户端回显自己 GET 到的
    

    ⇒ 每一次带守卫的保存都冲突,发生在没人碰过的记录上。而全部既有 OCC pin 在整个期间都是绿的 —— 因为它们两侧都跑 ISO 文本(memory / mock / SQLite 全都往返规范 ISO 串)。判别性输入是驱动给的 Date,而仓里从来没有任何东西把它喂进这条缝。

    ⇒ 这是**「绿的套件不是套件」那一类,而且是它最贵的形态:替身被引入的那条路正好就是它掩盖掉的那条路。#13382 的修复已落地且 pin 会响亮失败,所以现状不坏 ⇒ p2 而非 p1;但这张卡关的是让它当初能溜过去的那个洞**,不是补测试。

    派单说明

    • 选项 1 要 pin 的是「组合事实」:在 live 矩阵下,一行的 updated_at 在 Postgres/MySQL 上读回来是一个 Date,其 String() 丢毫秒且带进程时区。⭐ 这不是过度 pin —— 驱动自己就把这个决定写成了明文(withPostgresCalendarDayAsText:"timestamptz / timestamp are deliberately untouched: those are instants, a Date is the right materialisation for them"),pin 的是一个被刻意声明过的行为。
    • ⛔ 不要为此给 @objectstack/metadata-protocol 加驱动依赖。 卡片写得对:分层是反方向的,那个包必须不长驱动依赖。缝一侧的回归套件用手造 Date 是正确且足够的(它能抓住修复被回退),缺的只是驱动侧那一半。

    ⚠️ 卡片末尾那个「更大的问题」我按它的建议单独看待,⛔ 不并入本卡

    updated_at 不是唯一一个在 Date 驱动(Postgres / MySQL / MongoDB)与 ISO 文本驱动(SQLite 家族 / memory)之间运行时类型不同的值。

    这是一次 sweep,不是一次修复,卡片自己说「would be its own card」。⚠️ 但它目前只存在于本卡正文的一段散文里 —— 按本仓纪律,散文对候选查询与老化告警不可见。⇒ 若选项 1 落地时该 sweep 仍无卡,由执行席在同一笔立 finding,把「哪些消费者比较或格式化这类值、而只在文本侧被测过」写成可执行判据。⛔ 不要让它随本卡一起关掉。


    Generated by Claude Code

  3. claude commented on Aug 31, 2026

    @claude
    Contributor

    Claiming this card for implementation.

    • Session: session_01F3jdziLbAPGeceVNmSox5L
    • Branch: claude/issue-13567-live-pg-updated-at-characterisation

    Scope: option 1 only — a driver-side characterisation pin in driver-sql, inside the existing live matrix. Not touching packages/metadata-protocol (no driver dependency, and PR #13870 is in flight there), and not widening the required job's package set.


    Generated by Claude Code

  4. claude commented on Aug 31, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 13567,
      "status": "done",
      "branch": "claude/issue-13567-live-pg-updated-at-characterisation",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13971",
      "premise_still_valid": true,
      "summary": "Implemented option 1 and only option 1: one new test file, packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts, matrix-routed through declareDialectCell so it runs on SQLite plus the two live cells of Temporal Conformance (live PG + MySQL). Nothing else is touched — no metadata-protocol (PR 13870 is in flight in protocol.ts), no new package in any required job's set. Zone-2: A2.1 partly — 11224/11176/11223 read updated_at back on all three cells but through raw knex and a type-TOLERANT asInstant helper (Date | number | string), so nothing today can discriminate the shape; the new pin reads through findOne, the door probeRecord uses. A2.2 declareDialectCell, used verbatim; per-file schema isolation and the globalSetup both derive from a directory listing so the new file needs no registration. A2.3 FALSIFIED — the STOP condition does not fire: withPostgresCalendarDayAsText touches only OID 1082/1182 and says timestamptz/timestamp are deliberately untouched, and formatOutput's audit-timestamp repair plus its datetime normalisation are both inside if (this.isSqlite), so on the live dialects updated_at reaches the caller as the client materialised it; confirmed from the other side too, because the SQLite cell runs locally and observes ISO TEXT — a shared read-path normaliser could not produce that disagreement. A2.4 NO — repo-wide the only updated_at type assertions are in service-messaging against an outbox insert payload; the nearest thing in driver-sql is 11389's timestamptz instanceof Date, but for an ad-hoc SQL literal, PG only, not the audit column the seam reads. Also filed the wider sweep the card's closing paragraph declined and the triage note asked the execution seat to record.",
      "tests": "All readings at head aee4857c83 (git rev-parse --short HEAD), tree clean, remote head identical. Exit codes captured by redirect-then-read, never through a pipe. EXECUTED: (1) new file, ambient UTC — 6 passed, 2 named-skipped (live postgres, live mysql). (2) new file, TZ=America/New_York (the job's zone) — 6 passed, 2 named-skipped. (3) new file, OS_EXPECT_LIVE_DIALECT_MATRIX=1 with no URLs — 6 passed, 2 named RED (\"OS_TEST_POSTGRES_URL is unset while OS_EXPECT_LIVE_DIALECT_MATRIX=1 ... must not be skipped\"), which proves both live cells are declared and reachable rather than silently omitted. (4) whole driver-sql suite, TZ=America/New_York — \"Test Files 148 passed | 9 skipped (157) / Tests 2258 passed | 134 skipped (2392)\". (5) tsc --noEmit --listFiles for driver-sql — exit 0, and the new file IS in the compiled set (1 hit), so the reading really covers the edit (the package tsconfig includes src/**/* and excludes no tests). (6) pnpm lint (repo-wide eslint . --no-inline-config) — exit 0 in 61s, run WHOLE; no narrowing claimed. NOT MEASURED — the live cells did not execute: no OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL, nothing on 5432 or 3306, no docker daemon. Temporal Conformance in CI is this pin's FIRST real execution; a skipped cell is reported as a skip, never as a pass. ABLATION (two legs, both genuine reds). Leg 1: sqlite cell made to assert the LIVE cells' claim (updated_at instanceof Date) — sec B1 RED, which also shows SQLite genuinely does not hand back a Date. Leg 2: sqlite control inverted (String(v) NOT the echoed token) — sec B2 RED. Each leg proved the mutation on disk before running: removed anchor counted 1 before / 0 after, injected anchor 0 before / 1 after, and git hash-object moved off the HEAD blob 6640b2b1 to 39893824 and 1bb3d853. Restore under trap ... EXIT INT TERM with absolute paths and git checkout HEAD -- (never the bare form), proven by OBSERVATION not exit code: blob back to 6640b2b1 and git diff HEAD empty on both legs. No rebuild was needed or claimed — the file imports ./index.js relative, so vitest loads driver source, not dist. ⚠️ Recorded because it nearly produced a false green: the FIRST ablation run scored exit 1 on both legs while running NOTHING — a repo-relative path handed to a vitest whose cwd is the package, where \"No test files found, exiting with code 1\" is byte-identical to a red at the exit-code level. The script now refuses any leg whose log lacks a \"Tests N failed\" line; the numbers above are from the corrected run. Separately, sec A2's UTC cell went red on its own first run on Object.is(-0, 0) being false — the same negative-zero hazard the matrix testkit's own eastOfUtc records, found here by running; fixed with the reason written into the file. The two live cells cannot execute here, so THEIR ablation is NOT MEASURED and none is claimed. GATES — derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (26 families harvested with --commands so neither invocation spelling is dropped), every one run. 23 GREEN (exit 0): ci-filter-parity, comment-mask-adoption, cross-package-test-inputs (both spellings), keyed-text-bounds, plugin-teardown-shape, shard-attestation, undeclared-dep-imports, affected-docs, drift-comment, doc-authoring, driver-conformance, engine-double-contract, logger-receiver-detach, objectql-double-limit, page-declaration-shape, published-files, query-options-erasure, slot-lookup, test-source-alias, type-check-coverage, type-source-resolution, where-matcher — plus check-nul-bytes (7661 files, \"no raw ASCII control bytes\") and a direct grep -naP control-byte sweep of the new file (no matches). NOT MEASURED, three prerequisite refusals, none a pass and none a finding: check-test-completeness.mjs EXIT 3, its own words \"Nothing was measured: this gate exited before parsing a single summary line\" (needs a saved turbo run test log); check:dual-build-cjs-loads EXIT 3, \"PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/\" naming 78 packages; check:type-check-debt exit 1 but a REFUSAL not a ratchet failure — its self-test printed \"47 semantic case(s) ... hold\" and the structural half printed \"check-type-check-coverage: OK\", then --re-measure threw \"cannot run: 50 workspace dependenc(ies) ... have no built type entry point on disk\". Two independent reasons this diff cannot have moved that ratchet, stated as an argument and not as a measurement: @objectstack/driver-sql is in NEITHER the DEBT nor the TEST_DEBT ledger, so --re-measure has no entry for it, and the structural half — the one that would demand a TEST_DEBT entry for a package hiding its tests from tsc — ran and passed, consistent with the --listFiles reading above. NO ratchet counted up; no baseline updated; no gate's --update invoked.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probe returned HTTP 200 on this seat) plus git; no MCP GitHub call was made",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #13973: [finding] Sweep — which consumers compare or format a value whose runtime type differs between the Date-materialising drivers (Postgres, MySQL, MongoDB) and the ISO-text ones (SQLite family, memory). Unassigned, labelled `finding`, no `pm:queue`. This is the paragraph #13567's own body declined to answer and the triage comment asked the execution seat to file in the same stroke so it does not close with that card; dedup was a REST list over all 398 open issues plus local grep of titles AND bodies with a control-verified query (0 candidate hits; control matched #13567 only)."
      ]
    }

    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions