Skip to content

driver-sql: on MySQL the UPDATE door's updated_at stamp truncates to whole seconds against a DATETIME(3) column — a row updated in its first second reads updated_at EARLIER than created_at #11224

Closed

Description

@os-zhuang

Found while choosing the stamp expression for #11176's upsert path. Not fixed there: that card is about whether updated_at advances, this one is about the fidelity of the value it advances to, and fixing it changes the SQL every update() on MySQL emits.

The mismatch

packages/drivers/driver-sql/src/sql-driver.ts:

So the column is created with millisecond precision on purpose and then written at second precision.

Measured, live MySQL 8.0.46

Against the exact schema createAuditTimestampColumn produces:

AFTER INSERT (both columns from the DEFAULT now(3))
  created_at 2026-08-23 11:59:33.357   updated_at 2026-08-23 11:59:33.357

AFTER an update() stamp — CURRENT_TIMESTAMP, what updatedAtStamp() emits
  created_at 2026-08-23 11:59:33.357   updated_at 2026-08-23 11:59:33.000
  -> updated_at is EARLIER than created_at, by 357 ms

AFTER the precision-matched form CURRENT_TIMESTAMP(3)
  created_at 2026-08-23 11:59:33.357   updated_at 2026-08-23 11:59:33.361   ok

Consequences

  1. "Last modified" can precede "created". Any consumer comparing the two — an audit answer, a "modified since creation?" badge, a data-quality check — reads a row that was modified as if it were not.
  2. Delta / incremental sync can SKIP a row. A cursor held at millisecond precision (updated_at > cursor) misses every row whose stamp was truncated back below the cursor. That is the same silent-wrong-answer family as driver-sql: updated_at is never refreshed on a deployment that skips boot schema sync — tablesWithTimestamps is also only filled by DDL #11067 and driver-sql: updateMany() never stamps updated_at, and upsert()'s merge branch does not advance it on Postgres/MySQL — on every deployment, DDL or not #11176, reached by a different mechanism.
  3. Two updates in the same second are indistinguishable, so an ordering by updated_at is unstable exactly where it matters most.

Postgres is unaffected: CURRENT_TIMESTAMP there is transaction_timestamp() at microsecond precision, and the column is timestamptz. SQLite is unaffected: updatedAtStamp() returns a full ISO-8601 string with millis there.

This is already half-known in the repo — sql-driver-timestamps-without-ddl.test.ts documents the truncation in its BACKDATED_MS docblock as a reason to backdate rather than sleep, but records it as a testing hazard rather than as a defect in the stored value.

Suggested shape (not decided here)

Give updatedAtStamp() the same precision the column default carries — this.knex.fn.now(3) on MySQL, unchanged elsewhere. #11176's upsertUpdatedAtStamp() already does exactly this for the upsert door and carries the reasoning at the site; the two would then collapse into one helper. Deliberately left as two by that PR so the UPDATE door's emitted SQL was not changed by a card that had not measured it.

Note the ordering constraint if both land: whoever converges them owns re-running the live MySQL cells of the driver-sql matrix, since the emitted SQL for every update() on MySQL changes.


Generated by Claude Code

Activity

  1. added theissue type on Aug 23, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Aug 23, 2026
  3. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue, domain:engine, Bug — measured on live MySQL 8.0.46: updated_at written at second precision into a DATETIME(3) column whose DEFAULT is now(3); "modified before created" rows and ms-cursor delta-sync skips are user-reachable consequences. Fix shape inherited from #11176's upsertUpdatedAtStamp() (precision-matched now(3) on MySQL, unchanged elsewhere); converging the two helpers is in scope. Constraints: ① serial/fold with the sql-driver.ts family — #11223 and any in-flight #11176 work touch the same file; the engine seat answers fold-or-serial across the family with the five gates at claim time; ② whoever lands it owns re-running the live MySQL cells of the driver-sql matrix (emitted SQL for every update() on MySQL changes), per the card's own ordering note.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  5. self-assigned this
    on Aug 23, 2026
  6. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Claim

    Seat domain:engine · session session_01RfyXxZ2WPjcjhuXpiQQc3y · branch claude/issue-11224-mysql-update-stamp-precision

    Filed by this seat's dev while choosing the stamp expression for #11176; promoted by triage. Duplicate check clean, both queries: repo-scoped PR search returns nothing, and git ls-remote origin 'refs/heads/*issue-11224*' returns 0 branches — with a positive control (issue-9167 returns 2), so the zero is a reading.

    ⭐ This card is NOT a fresh design — the correct form already exists and is already measured

    #11176 landed upsertUpdatedAtStamp() for the upsert door precisely because the bare form was measured wrong there, and its dev wrote the follow-up plan down: "#11176's upsertUpdatedAtStamp() already carries the correct form for the upsert door; the two collapse into one helper when #11224 lands."

    ⇒ The work is generalising an already-verified expression to the UPDATE door and collapsing the pair into one helper, not deciding what the right stamp is. ⛔ Do not re-derive it.

    The defect, as measured on live MySQL 8.0.46

    updatedAtStamp() is a bare knex.fn.now(), which compiles to an unqualified CURRENT_TIMESTAMP — truncated to whole seconds — against a DATETIME(3) audit column whose own DEFAULT is now(3) (createAuditTimestampColumn's docblock states the precision requirement in as many words):

    created_at              11:59:33.357
    update() writes         11:59:33.000   ← updated_at 357 ms EARLIER than created_at
    CURRENT_TIMESTAMP(3)    11:59:33.361   ← the precision-matched form, correct
    

    Three consequences, all silent: "last modified" can precede "created"; a millisecond-precision delta cursor SKIPS rows whose stamp was truncated back below it; two updates within the same second are indistinguishable.

    ⚠️ This is the same silent-wrong-answer family as #11067 / #11176 / #11223 by a fourth mechanism. It is the last one this seat knows about in that family.

    ⛔ Serialization: the file is occupied by ANOTHER SEAT, and the reasoning matters

    packages/drivers/driver-sql/src/sql-driver.ts is currently also being changed by PR #11270 (spec seat, card #11122, claude/issue-11122-introspection-contract, draft since 07:24Z, not enqueued). It touches the file's introspection region — withdrawing the indexes promise, widening defaultValue/maxLength — plus schema-drift.ts and packages/spec.

    This seat's same-file rule is not being applied against it, deliberately:

    1. Same-file serialization can only ever bind this seat's own devs. Another seat does not stop because I hold a file, and the repo's own measurement says shared-path concurrency is the norm (check-single-claim-paths.mjs: 68 concurrent pairs / 300 PRs; the at-most-one-writer list has exactly one entry, and it is not this file).
    2. The protocol's own carve-out applies: integral-file serialization is the fallback for when the region cannot be stated clearly. Here it can — spec: withdraw the never-honored introspection indexes promise; widen defaultValue/maxLength to the measured emitted types #11270 is in the introspection region, this card is in the write-door stamp. No semantic overlap, and they sit far apart in the file.

    ⚠️ #11201 is a different story and stays held: PG introspectForeignKeys is the same region #11270 is rewriting. That one waits for #11270, and that is a technical judgement about overlap, not a protocol requirement.

    Boundaries

    1. ⚠️ Rebase awareness, not avoidance: spec: withdraw the never-honored introspection indexes promise; widen defaultValue/maxLength to the measured emitted types #11270 will land changes elsewhere in this file. Branch from current main, and if it merges first, rebase rather than fight it. ⛔ Do not touch the introspection region.
    2. ⚠️ Two pm:on-hold neighbours live in this file: drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740 (restart = insertOnlyUpsertColumns gains a member / a fourth dialect / a conflict-target column gains a non-null DEFAULT) and drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009 (restart = touching sqliteCanonicalDatetimeSql or backfillCanonicalDatetimes). ⛔ Touch either symbol ⇒ stop and report; do not repair them in passing.
    3. ⚠️ This changes the SQL every update() on MySQL emits — the card itself flags that as a verification surface driver-sql: updateMany() never stamps updated_at, and upsert()'s merge branch does not advance it on Postgres/MySQL — on every deployment, DDL or not #11176 did not measure. Live MySQL evidence is required, not a unit-test-only argument.
    4. ⛔ packages/spec off limits (and spec: withdraw the never-honored introspection indexes promise; widen defaultValue/maxLength to the measured emitted types #11270 is already there — ⛔ do not collide with it). ⛔ Never edit content/docs/releases/. Breaking changes ship minor, never major.
    5. ⛔ No skipped/disabled/quarantined tests; ⛔ no raised ratchet ceilings.
    6. ⚠️ sql-driver.ts carries an ADR-0120 anchor roster that dispatch-gates.mjs does not select — run check:adr-anchors by hand.
    7. ⚠️ A zero-match pnpm --filter <pkg> <script> exits 0; read the tool's printed lines and confirm the script name was echoed. A gate failing on an unbuilt worktree is NOT MEASURED, not a pass.

    Generated by Claude Code

  7. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 11224,
      "status": "done",
      "branch": "claude/issue-11224-mysql-update-stamp-precision",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11320",
      "premise_still_valid": true,
      "summary": "Verified the card's premise against origin/main before writing code: `updatedAtStamp()` was a bare `knex.fn.now()` on non-SQLite, `createAuditTimestampColumn` really does build MySQL audit columns as DATETIME(3) DEFAULT now(3), and #11176's `upsertUpdatedAtStamp()` was still a separate helper carrying the precision-matched form. Generalised that already-measured expression to the UPDATE door (now(3) on MySQL, unchanged on PG/SQLite) and COLLAPSED THE PAIR: `upsertUpdatedAtStamp()` is deleted and `stampUpsertUpdatedAt` reads `updatedAtStamp()`, so all four stamping doors (update, updateMany, rotatedUpdateById, upsert merge) share one definition. Because the removed helper is a `protected` member that shipped in 17.2.0, the changeset declares BREAKING at `minor` (never major) and carries an ADR-0087 `not-required (no-migration-prescription)` disposition; `content/docs/releases/` untouched. Neither pm:on-hold neighbour is touched. Two live DB services (MySQL 8.0.46, Postgres 16) were started in this shared container for the measurements and were deliberately LEFT RUNNING rather than stopped: they are system daemons, not my child processes, and a sibling agent's live-dialect-matrix run could be using them — stopping them is PM's call, not a silent teardown.",
      "tests": "LIVE BEFORE/AFTER, MySQL 8.0.46, raw SQL against the exact DDL createAuditTimestampColumn emits (`datetime(3) default current_timestamp(3)`): before/CURRENT_TIMESTAMP -> created_at 2026-08-23 10:22:36.799, updated_at 2026-08-23 10:22:36.000, delta -799.0000 ms, '*** updated_at BEFORE created_at ***'; after/CURRENT_TIMESTAMP(3) -> created_at 10:22:36.799, updated_at 10:22:36.802, delta +3.0000 ms, 'ok'. FAILING BASELINE DEMONSTRATED FIRST, through the real driver: the new suite was written and run against the UNMODIFIED driver -> 'Tests  6 failed | 15 passed (21)', e.g. 's0: updated_at 2026-08-23T10:19:37.000Z is EARLIER than created_at 2026-08-23T10:19:37.911Z' (911 ms), 'rows skipped by their own delta cursor after update(): c0, c1, c2, c3, c4, c5', 'every update in this second stamped the SAME instant: expected 1 to be greater than 1'. All 6 reds were on the live-mysql cell; SQLite 7/7 and live Postgres 7/7 green in that same baseline run — that IS the other-dialects measurement, repeated green after the fix. Baseline was run BEFORE any edit, so no revert/restore step was needed and no measurement is post-hoc. AFTER: `pnpm --filter @objectstack/driver-sql test` with TZ=America/New_York, OS_TEST_MYSQL_URL + OS_TEST_POSTGRES_URL set and OS_EXPECT_LIVE_DIALECT_MATRIX=1, servers skewed as CI skews them (MySQL @@global.time_zone='+08:00', PG timezone='Asia/Shanghai') -> 'Test Files  121 passed (121)' / 'Tests  2488 passed (2488)', VERDICT command-exit 0. DOWNSTREAM direction, prefix form `...@objectstack/driver-sql` = the package plus its 47 DEPENDENTS: `turbo run typecheck` 76/78 tasks green on the first pass; the only failure was @objectstack/plugin-auth reporting TS7016 'Could not find a declaration file for module @objectstack/plugin-auth' in its OWN examples/basic-usage.ts — a cold-worktree race between its build's DTS step and its own typecheck, i.e. NOT MEASURED rather than a fail; built alone and re-run it is exit 0 ('tsc --noEmit && tsc --noEmit -p tsconfig.examples.json'). REPO-WIDE `pnpm lint` (eslint . --no-inline-config) run in full, exit 0 in 2m03s — NO NARROWING DECLARED anywhere in this task. GATES: `node scripts/pm/dispatch-gates.mjs` run with NO paths passed (it derived the change set itself: 5 paths vs merge base f24c90df3); every derived family plus every convention-triggered one plus `check:adr-anchors` by hand (the ADR-0120 roster the derivation does not select) plus `check:nul-bytes` — all exit 0, each captured before any pipe via `cmd > file 2>&1; EXIT=$?`: adr-anchors 0, nul-bytes 0, changeset-gate-self-tests 0, driver-conformance 0, objectui-changeset 0, published-files 0, slot-lookup 0, test-source-alias 0, type-source-resolution 0, check-adr-0087-registration 0, check-changeset-no-major 0, check-ci-filter-parity 0, check-empty-changeset 0, check-plugin-teardown-shape 0, docs-audit/check-affected-docs 0, query-options-erasure 0, type-check-coverage 0, type-check-debt 0, engine-double-contract 0, cross-package-test-inputs 0, where-matcher 0, lint 0. The three changeset gates were re-run AFTER committing (they diff HEAD, so a staged-only tree reads as '0 changesets seen'); post-commit check-adr-0087-registration reports '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'. The ratchet family was re-run on the FINAL commit c46bb3405 and reports against it: 'slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new', 'query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new', 'where-matcher conformance holds: 283 matcher(s) discovered, 283 answer the combinator battery correctly or refuse it loudly'. check:type-check-debt ran on a fully built workspace (turbo build --filter=./packages/* --filter=./packages/*/* exit 0 first), so its verdict is a measurement: 'OK — 33 ledger entr(ies) re-measured in 325.1s, 1897 raw tsc error(s) total, none above its recorded number.' No ratchet ceiling raised, no test skipped/disabled/quarantined. ON-HOLD SYMBOL COUNTS over the diff's changed lines, with controls: insertOnlyUpsertColumns 0, sqliteCanonicalDatetimeSql 0, backfillCanonicalDatetimes 0; POSITIVE CONTROL, same literals over sql-driver.ts as it stands: 2 / 10 / 7 occurrences respectively, and a second control feeding the diff filter a synthetic '+  this.insertOnlyUpsertColumns(object);' line returns 1 — so the three zeros are readings, not an unarmed grep. Also verified: NO in-repo subclass overrides either helper (SqliteWasmDriver and TursoDriver override neither and are SQLite-family, so they take the unchanged branch). Zero-match pnpm filter trap avoided: every run's output was checked for the echoed script name rather than a bare $?. NO ABLATION was performed in this task, so no rebuild/mutation-on-disk claim is made — the baseline red was obtained by running the new suite against unmodified source, which needs no mutation.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11321: on SQLite `createAuditTimestampColumn` gives the builtin audit columns `default CURRENT_TIMESTAMP` (zone-naive, space-separated, second-precision — measured: an out-of-band insert stores \"2026-08-23 11:03:23\") while a declared Field.datetime NOW() default in the SAME table gets the canonical `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))` and stores \"2026-08-23T11:03:23.611Z\". Unassigned, no labels; searched first (repo-scoped issue search + local scan, no existing card). Not repaired in passing: different door, different dialect, and it changes DDL — a new verification surface, so it fails the bounded-exemption test. Flagged in the card that a fix reaching for backfillCanonicalDatetimes / sqliteCanonicalDatetimeSql collides with pm:on-hold #6009."
      ]
    }

    Two bounded in-place repairs are declared in the PR body rather than left silent: removing upsertUpdatedAtStamp() left three docblocks (two BACKDATED_MS rationales and one §4 head note, across sql-driver-11176-bulk-and-merge-updated-at.test.ts and sql-driver-timestamps-without-ddl.test.ts) asserting driver behaviour that is false as of this commit — one of them recording the very defect this card fixes as if it were a property of MySQL. All three edits are comment-only; no assertion, fixture or executed line changed, and both files pass unchanged.


    Generated by Claude Code

  8. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report

    Marker note (read this first). The previous comment (#issuecomment-5385669065) carried the same report but opened with the HTML-comment form of the marker. Read back after posting, GitHub's sanitizer had eaten it — the stored body starts at the fenced block, so that comment is invisible to a marker scan. No comment-edit tool is exposed to this seat, so this comment supersedes it with the marker as literal text. This is the authoritative report; the earlier one is a duplicate, not a second run.

    {
      "issue": 11224,
      "status": "done",
      "branch": "claude/issue-11224-mysql-update-stamp-precision",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11320",
      "premise_still_valid": true,
      "summary": "Verified the card's premise against origin/main before writing code: `updatedAtStamp()` was a bare `knex.fn.now()` on non-SQLite, `createAuditTimestampColumn` really does build MySQL audit columns as DATETIME(3) DEFAULT now(3), and #11176's `upsertUpdatedAtStamp()` was still a separate helper carrying the precision-matched form. Generalised that already-measured expression to the UPDATE door (now(3) on MySQL, unchanged on PG/SQLite) and COLLAPSED THE PAIR: `upsertUpdatedAtStamp()` is deleted and `stampUpsertUpdatedAt` reads `updatedAtStamp()`, so all four stamping doors (update, updateMany, rotatedUpdateById, upsert merge) share one definition. Because the removed helper is a `protected` member that shipped in 17.2.0, the changeset declares BREAKING at `minor` (never major) and carries an ADR-0087 `not-required (no-migration-prescription)` disposition; `content/docs/releases/` untouched. Neither pm:on-hold neighbour is touched. Two live DB services (MySQL 8.0.46, Postgres 16) were started in this shared container for the measurements and were deliberately LEFT RUNNING rather than stopped: they are system daemons, not my child processes, and a sibling agent's live-dialect-matrix run could be using them — stopping them is PM's call, not a silent teardown. Worktree torn down without --force after the PR opened; the sibling worktree objectstack-issue-11201 was left untouched.",
      "tests": "LIVE BEFORE/AFTER, MySQL 8.0.46, raw SQL against the exact DDL createAuditTimestampColumn emits (`datetime(3) default current_timestamp(3)`): before/CURRENT_TIMESTAMP -> created_at 2026-08-23 10:22:36.799, updated_at 2026-08-23 10:22:36.000, delta -799.0000 ms, '*** updated_at BEFORE created_at ***'; after/CURRENT_TIMESTAMP(3) -> created_at 10:22:36.799, updated_at 10:22:36.802, delta +3.0000 ms, 'ok'. FAILING BASELINE DEMONSTRATED FIRST, through the real driver: the new suite was written and run against the UNMODIFIED driver -> 'Tests  6 failed | 15 passed (21)', e.g. 's0: updated_at 2026-08-23T10:19:37.000Z is EARLIER than created_at 2026-08-23T10:19:37.911Z' (911 ms), 'rows skipped by their own delta cursor after update(): c0, c1, c2, c3, c4, c5', 'every update in this second stamped the SAME instant: expected 1 to be greater than 1'. All 6 reds were on the live-mysql cell; SQLite 7/7 and live Postgres 7/7 green in that same baseline run — that IS the other-dialects measurement, repeated green after the fix. Baseline was run BEFORE any edit, so no revert/restore step was needed and no measurement is post-hoc. AFTER: `pnpm --filter @objectstack/driver-sql test` with TZ=America/New_York, OS_TEST_MYSQL_URL + OS_TEST_POSTGRES_URL set and OS_EXPECT_LIVE_DIALECT_MATRIX=1, servers skewed as CI skews them (MySQL @@global.time_zone='+08:00', PG timezone='Asia/Shanghai') -> 'Test Files  121 passed (121)' / 'Tests  2488 passed (2488)', VERDICT command-exit 0. DOWNSTREAM direction, prefix form `...@objectstack/driver-sql` = the package plus its 47 DEPENDENTS: `turbo run typecheck` 76/78 tasks green on the first pass; the only failure was @objectstack/plugin-auth reporting TS7016 'Could not find a declaration file for module @objectstack/plugin-auth' in its OWN examples/basic-usage.ts — a cold-worktree race between its build's DTS step and its own typecheck, i.e. NOT MEASURED rather than a fail; built alone and re-run it is exit 0 ('tsc --noEmit && tsc --noEmit -p tsconfig.examples.json'). REPO-WIDE `pnpm lint` (eslint . --no-inline-config) run in full, exit 0 in 2m03s — NO NARROWING DECLARED anywhere in this task. GATES: `node scripts/pm/dispatch-gates.mjs` run with NO paths passed (it derived the change set itself: 5 paths vs merge base f24c90df3); every derived family plus every convention-triggered one plus `check:adr-anchors` by hand (the ADR-0120 roster the derivation does not select) plus `check:nul-bytes` — all exit 0, each captured before any pipe via `cmd > file 2>&1; EXIT=$?`: adr-anchors 0, nul-bytes 0, changeset-gate-self-tests 0, driver-conformance 0, objectui-changeset 0, published-files 0, slot-lookup 0, test-source-alias 0, type-source-resolution 0, check-adr-0087-registration 0, check-changeset-no-major 0, check-ci-filter-parity 0, check-empty-changeset 0, check-plugin-teardown-shape 0, docs-audit/check-affected-docs 0, query-options-erasure 0, type-check-coverage 0, type-check-debt 0, engine-double-contract 0, cross-package-test-inputs 0, where-matcher 0, lint 0. The three changeset gates were re-run AFTER committing (they diff HEAD, so a staged-only tree reads as '0 changesets seen'); post-commit check-adr-0087-registration reports '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'. The ratchet family was re-run on the FINAL commit c46bb3405 and reports against it: 'slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new', 'query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new', 'where-matcher conformance holds: 283 matcher(s) discovered, 283 answer the combinator battery correctly or refuse it loudly'. check:type-check-debt ran on a fully built workspace (turbo build --filter=./packages/* --filter=./packages/*/* exit 0 first), so its verdict is a measurement: 'OK — 33 ledger entr(ies) re-measured in 325.1s, 1897 raw tsc error(s) total, none above its recorded number.' No ratchet ceiling raised, no test skipped/disabled/quarantined. ON-HOLD SYMBOL COUNTS over the diff's changed lines, with controls: insertOnlyUpsertColumns 0, sqliteCanonicalDatetimeSql 0, backfillCanonicalDatetimes 0; POSITIVE CONTROL, same literals over sql-driver.ts as it stands: 2 / 10 / 7 occurrences respectively, and a second control feeding the diff filter a synthetic '+  this.insertOnlyUpsertColumns(object);' line returns 1 — so the three zeros are readings, not an unarmed grep. Also verified: NO in-repo subclass overrides either helper (SqliteWasmDriver and TursoDriver override neither and are SQLite-family, so they take the unchanged branch). Zero-match pnpm filter trap avoided: every run's output was checked for the echoed script name rather than a bare $?. NO ABLATION was performed in this task, so no rebuild/mutation-on-disk claim is made — the baseline red was obtained by running the new suite against unmodified source, which needs no mutation.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #11321: on SQLite `createAuditTimestampColumn` gives the builtin audit columns `default CURRENT_TIMESTAMP` (zone-naive, space-separated, second-precision — measured: an out-of-band insert stores '2026-08-23 11:03:23') while a declared Field.datetime NOW() default in the SAME table gets the canonical `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))` and stores '2026-08-23T11:03:23.611Z'. Unassigned, no labels; searched first (repo-scoped issue search + local scan, no existing card). Not repaired in passing: different door, different dialect, and it changes DDL — a new verification surface, so it fails the bounded-exemption test. Flagged in the card that a fix reaching for backfillCanonicalDatetimes / sqliteCanonicalDatetimeSql collides with pm:on-hold #6009."
      ]
    }

    Two bounded in-place repairs are declared in the PR body rather than left silent: removing upsertUpdatedAtStamp() left three docblocks (two BACKDATED_MS rationales and one §4 head note, across sql-driver-11176-bulk-and-merge-updated-at.test.ts and sql-driver-timestamps-without-ddl.test.ts) asserting driver behaviour that is false as of this commit — one of them recording the very defect this card fixes as if it were a property of MySQL. All three edits are comment-only; no assertion, fixture or executed line changed, and both files pass unchanged.


    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

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions