diff --git a/.changeset/tidy-jars-shake.md b/.changeset/tidy-jars-shake.md new file mode 100644 index 0000000000..7c08b8249e --- /dev/null +++ b/.changeset/tidy-jars-shake.md @@ -0,0 +1,28 @@ +--- +'@objectstack/metadata-protocol': patch +'@objectstack/rest': patch +--- + +Correct the out-of-package comments that still described `SqlDriver#formatOutput`'s +two timestamp passes as gated on `if (this.isSqlite)`. + +Since ADR-0053 D-F1 (#13973) both passes — the `AUDIT_TIMESTAMP_COLUMNS` pass and the +`normalizeSqliteDatetimeOutput` pass over `datetimeFields` — run on every dialect, so a +declared `Field.datetime` and the builtin audit columns are presented as canonical +ISO-8601-`Z` text on Postgres and MySQL as well as SQLite. The `rest-server.ts` comment +went further than staleness: it warned future authors that "a declared `Field.datetime` +is therefore NOT protected on Postgres/MySQL", inviting exactly the tolerant consumer-side +coercion ADR-0053 forbids. + +Comments only — no runtime behaviour, no exported symbol and no public type changes; the +published `.d.ts` of both packages is byte-identical. These two packages are named because +their bundled `dist/index.js` / `dist/index.cjs` carry the amended comment text verbatim, +so the published output does change. `@objectstack/metadata` carries the same correction +in `database-loader.ts` but is deliberately NOT named: its edits are all JSDoc blocks, +which its bundle strips, so its published output is unchanged. + +Two carve-outs are preserved rather than flattened: `withPostgresCalendarDayAsText` is +untouched by that ruling (D-F2 — the client library still materialises `timestamptz` / +`DATETIME(3)` as a `Date`; the driver now folds it at its own read boundary), and the +Invalid `Date` residue still stands (D-F3 — the one `Date` shape with no canonical text +leaves the read door unchanged). diff --git a/packages/metadata-protocol/src/sys-metadata-repository-14037-event-ts-canonicalisation.test.ts b/packages/metadata-protocol/src/sys-metadata-repository-14037-event-ts-canonicalisation.test.ts index 70a5247749..b32bdd746e 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository-14037-event-ts-canonicalisation.test.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository-14037-event-ts-canonicalisation.test.ts @@ -13,11 +13,20 @@ * straight past it into the declared field. * * `recorded_at` is a declared `Field.datetime` on `sys_metadata_history`, and - * that does NOT protect it: `SqlDriver#formatOutput` folds declared datetime - * columns (`normalizeSqliteDatetimeOutput`) only inside its - * `if (this.isSqlite)` arm, and `withPostgresCalendarDayAsText` leaves - * `timestamptz` / `timestamp` deliberately untouched. Pinned live in - * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`. + * when this landed that did NOT protect it: `SqlDriver#formatOutput` folded + * declared datetime columns (`normalizeSqliteDatetimeOutput`) only inside its + * `if (this.isSqlite)` arm. #13973 ([ADR-0053 D-F1]) has since lifted that fold + * out of the gate — it runs on every dialect — and the pin that recorded the + * asymmetry now records the canonical-text contract + * (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts` + * §B, inverted on purpose). + * + * ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling and still + * leaves `timestamptz` / `timestamp` deliberately alone ([ADR-0053 D-F2]) — the + * client still hands back a `Date`; the driver folds it at its own read + * boundary now. And the `Date` this file plants stays reachable: an INVALID + * `Date` leaves `driver-sql` unchanged ([ADR-0053 D-F3]) and non-SQL drivers + * materialise their own, so what is pinned below is a live adapter arm. * * ## Why it matters downstream, not just as a type * diff --git a/packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts b/packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts index 21a9fb142c..6c03d74ab8 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts @@ -17,20 +17,29 @@ * declaration is an inline return type), and `rows` is cast `as any[]` one line * above the map, so tsc sees a `string` assignment that never happened. * - * ## Why the value is a `Date` on the live dialects + * ## Why a `Date` reaches this projection at all * * `updated_at` / `created_at` are the BUILTIN audit columns on `sys_metadata` * (`Field.datetime`, `packages/metadata-core/src/objects/sys-metadata.object.ts`). - * `SqlDriver#formatOutput` repairs the audit columns - * (`repairNaiveUtcAuditTimestamp`) and folds the declared datetime columns + * When this landed, `SqlDriver#formatOutput` repaired the audit columns + * (`repairNaiveUtcAuditTimestamp`) and folded the declared datetime columns * (`normalizeSqliteDatetimeOutput`) ONLY inside its `if (this.isSqlite)` arm, - * and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` - * deliberately untouched because those are instants. That dialect fact is - * pinned live in - * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`; - * this file does not re-derive it and takes on no driver dependency - * (`@objectstack/metadata-protocol` has none, and the layering runs the other - * way) — the `Date` is hand-made here for exactly that reason. + * so both live dialects handed the column through as a JS `Date`. #13973 + * ([ADR-0053 D-F1]) has since lifted both passes out of that gate — they run on + * every dialect — and the pin that recorded the asymmetry now records the + * canonical-text contract + * (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts` + * §B, inverted on purpose). + * + * ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling and still + * leaves `timestamptz` / `timestamp` deliberately alone ([ADR-0053 D-F2]) — + * those are instants and the CLIENT still materialises them as a `Date`; what + * changed is that the driver folds it at its own read boundary. The `Date` this + * file plants therefore still reaches the projection in practice: an INVALID + * `Date` leaves `driver-sql` unchanged ([ADR-0053 D-F3]) and non-SQL drivers + * materialise their own. This file does not re-derive any of it and takes on no + * driver dependency (`@objectstack/metadata-protocol` has none, and the + * layering runs the other way) — the `Date` is hand-made here for that reason. * * ## What is asserted, and why it is not a hand-copied shape * diff --git a/packages/metadata-protocol/src/sys-metadata-repository.ts b/packages/metadata-protocol/src/sys-metadata-repository.ts index 201199a1a6..11c3269c89 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.ts @@ -90,14 +90,24 @@ import { isWritablePackage } from './package-writability.js'; * * [#13997] `sys_metadata`'s `created_at` / `updated_at` are BUILTIN audit * columns; `sys_metadata_history`'s `recorded_at` is a declared - * `Field.datetime`. On the live dialects BOTH arrive out of the record read - * door as a JS `Date`: `SqlDriver#formatOutput` repairs the audit columns - * (`repairNaiveUtcAuditTimestamp`) and folds the declared datetime columns - * (`normalizeSqliteDatetimeOutput`) ONLY inside its `if (this.isSqlite)` arm, - * and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` - * deliberately untouched because "those are instants, a `Date` is the right - * materialisation for them, and `Field.datetime` depends on it". Pinned in - * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`. + * `Field.datetime`. On the live dialects BOTH used to arrive out of the record + * read door as a JS `Date`: `SqlDriver#formatOutput` repaired the audit columns + * (`repairNaiveUtcAuditTimestamp`) and folded the declared datetime columns + * (`normalizeSqliteDatetimeOutput`) ONLY inside its `if (this.isSqlite)` arm. + * #13973 ([ADR-0053 D-F1]) lifted both passes out of that gate — they run on + * EVERY dialect now, so the read door presents the canonical ISO-Z text. + * Pinned in + * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts` + * §B, which was inverted on purpose to record the new contract. + * + * ⚠️ `withPostgresCalendarDayAsText` still leaves `timestamptz` / `timestamp` + * deliberately untouched ([ADR-0053 D-F2]) — those are instants and a `Date` + * remains the right materialisation for them at the CLIENT layer. What changed + * is that the driver no longer lets that `Date` out of its read door. (⛔ The + * clause this comment used to quote alongside it — that `Field.datetime` + * "depends on" the `Date` materialisation — was checked against the tree by + * #13973 and did not hold; it is gone from the driver and must not be quoted + * back.) * * `MetadataItem.authoredAt` is declared `z.string()` ('ISO-8601 timestamp', * `packages/metadata-core/src/types.ts`) and `MetadataItem` is a `z.infer`, so @@ -163,10 +173,14 @@ function canonicalIsoInstant(value: unknown): string | undefined { * [#14037] `rowToEvent` reaches `ts` through `(row.recorded_at as string) ?? * …`, and `row` is `any`, so tsc sees a `string` assignment that never * happened. `recorded_at` is a declared `Field.datetime` on - * `sys_metadata_history`, which the dialect asymmetry above does NOT protect: - * the `datetimeFields` fold sits inside `formatOutput`'s `if (this.isSqlite)` - * arm, so Postgres and MySQL hand the column out as a JS `Date`. - * `MetadataEventSchema.ts` is `z.string()` + * `sys_metadata_history`, and the dialect asymmetry described above did not + * protect it: the `datetimeFields` fold sat inside `formatOutput`'s + * `if (this.isSqlite)` arm, so Postgres and MySQL handed the column out as a JS + * `Date`. #13973 ([ADR-0053 D-F1]) has since closed that asymmetry — the fold + * runs on every dialect — but the cast is still an assertion rather than a + * measurement, and the `Date` domain is not empty: an INVALID `Date` still + * leaves `driver-sql` unchanged ([ADR-0053 D-F3]) and non-SQL drivers + * materialise their own. `MetadataEventSchema.ts` is `z.string()` * (`packages/metadata-core/src/types.ts`), and the value's one in-repo reader * — `MetadataManager.applyRepoEvent`, which forwards it to * `MetadataWatchEvent.timestamp` — is declared `z.string().datetime()`. @@ -1177,16 +1191,22 @@ export class SysMetadataRepository implements MetadataRepository { organizationId: row.organization_id ?? null, packageId: row.package_id ?? null, // [#14938] `updated_at` / `created_at` are the BUILTIN audit columns, - // so on Postgres and MySQL they arrive here as a JS `Date`: the audit - // repair and the declared-datetime fold both sit inside - // `SqlDriver#formatOutput`'s `if (this.isSqlite)` arm, and - // `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` - // alone because those are instants. `rows` is cast `as any[]` above, - // so tsc never saw the `Date` land in a field this signature declares - // `string | null`. Canonicalised at the producer — the same adapter - // boundary `rowToItem` uses, never a tolerant `??` in the console or a - // reshape at the driver's read door (#13973's two standing - // prohibitions). + // and on Postgres and MySQL they used to arrive here as a JS `Date`: + // the audit repair and the declared-datetime fold both sat inside + // `SqlDriver#formatOutput`'s `if (this.isSqlite)` arm. #13973 + // ([ADR-0053 D-F1]) lifted both out of that gate, so the read door now + // presents canonical ISO-Z text on every dialect; + // `withPostgresCalendarDayAsText` is untouched by that ruling and still + // leaves `timestamptz` / `timestamp` alone at the CLIENT parser + // ([ADR-0053 D-F2]). `rows` is cast `as any[]` above, so tsc never saw + // the `Date` land in a field this signature declares `string | null`, + // and the shape is still reachable: an INVALID `Date` leaves the driver + // unchanged ([ADR-0053 D-F3]) and non-SQL drivers materialise their own. + // Canonicalised at the producer — the same adapter boundary `rowToItem` + // uses, and ⛔ never a tolerant `??` in the console, which is the #13973 + // prohibition that still stands. (Its second one — ⛔ no unilateral + // reshape at the driver's read door — was DISCHARGED by the B1 ruling, + // which made that reshape the central fix rather than a local one.) // // The terminal is chosen PER CALL SITE (#14078) and this one is // `null`, not `rowToItem`'s `?? new Date(...).toISOString()`: this diff --git a/packages/metadata/src/loaders/database-loader-14037-adapter-boundary-iso.test.ts b/packages/metadata/src/loaders/database-loader-14037-adapter-boundary-iso.test.ts index 356dfb3dd6..2183d87266 100644 --- a/packages/metadata/src/loaders/database-loader-14037-adapter-boundary-iso.test.ts +++ b/packages/metadata/src/loaders/database-loader-14037-adapter-boundary-iso.test.ts @@ -13,15 +13,24 @@ * casts — an assertion about a driver row, never a measurement of one — which * is why tsc reported nothing. * - * On Postgres and MySQL the assertion is false for BOTH column classes: - * `SqlDriver#formatOutput` repairs the builtin audit columns - * (`repairNaiveUtcAuditTimestamp`) and folds declared `Field.datetime` columns + * When this landed, the assertion was false on Postgres and MySQL for BOTH + * column classes: `SqlDriver#formatOutput` repaired the builtin audit columns + * (`repairNaiveUtcAuditTimestamp`) and folded declared `Field.datetime` columns * (`normalizeSqliteDatetimeOutput`) only inside its `if (this.isSqlite)` arm, - * and `withPostgresCalendarDayAsText` leaves `timestamptz` / `timestamp` - * deliberately untouched. That dialect fact is pinned live in - * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`. - * `recorded_at` being a declared `Field.datetime` on `sys_metadata_history` - * does NOT protect it — the fold is inside the SQLite arm too. + * so `recorded_at` being a declared `Field.datetime` on `sys_metadata_history` + * did not protect it either. #13973 ([ADR-0053 D-F1]) has since lifted both + * passes out of that gate — they run on EVERY dialect — and the pin that + * recorded the asymmetry now records the canonical-text contract + * (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts` + * §B, inverted on purpose). + * + * ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling and still + * leaves `timestamptz` / `timestamp` deliberately alone ([ADR-0053 D-F2]) — the + * client still hands back a `Date`; the driver now folds it at its own read + * boundary. And the `Date` domain these cases pin did not close: an INVALID + * `Date` still leaves `driver-sql` unchanged ([ADR-0053 D-F3]), and non-SQL + * drivers materialise their own. So these cases pin a live adapter arm, not a + * historical one — what they own is the adapter's behaviour per input shape. * * All three declarations are `z.string().datetime()` * (`packages/spec/src/system/metadata-persistence.zod.ts`), a refinement a diff --git a/packages/metadata/src/loaders/database-loader.ts b/packages/metadata/src/loaders/database-loader.ts index ab705f3c85..a907523dab 100644 --- a/packages/metadata/src/loaders/database-loader.ts +++ b/packages/metadata/src/loaders/database-loader.ts @@ -37,14 +37,23 @@ import { migrateProjectIdToEnvironmentId } from '../migrations/migrate-project-i * `MetadataStats.mtime` is declared as. * * [#13997] `sys_metadata`'s `created_at` / `updated_at` are BUILTIN audit - * columns, so no declared-field coercion reaches them and - * `SqlDriver#formatOutput` repairs them only inside its `if (this.isSqlite)` - * arm. On Postgres and MySQL they arrive out of the record read door as a JS - * `Date` — pinned in - * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`. + * columns, so no declared-field coercion reaches them; `SqlDriver#formatOutput` + * repairs them through its own `AUDIT_TIMESTAMP_COLUMNS` pass, which used to + * sit inside `if (this.isSqlite)` and so handed Postgres and MySQL rows out of + * the record read door as a JS `Date`. #13973 ([ADR-0053 D-F1]) lifted that + * pass out of the gate — it now runs on EVERY dialect, and the pin that once + * recorded the asymmetry records the canonical-text contract instead + * (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts` + * §B, inverted on purpose). + * * `MetadataStatsSchema.mtime` is `z.string().datetime()` * (`packages/spec/src/system/metadata-persistence.zod.ts`), so a `Date` here - * is a silent violation of a declared contract. + * is a silent violation of a declared contract. This adapter stays because the + * `Date` domain did not close: `driver-sql` still hands an INVALID `Date` + * through unchanged ([ADR-0053 D-F3] — the one shape with no canonical text), + * and non-SQL drivers materialise their own `Date`s. Under [ADR-0053 D-F1]'s + * B1 ruling a producer-side arm like this became a NO-OP for the valid-`Date` + * case, ⛔ never a conflict with it. * * ⚠️ The call below looks redundant against `MetadataRecord`'s static type and * is not: `rowToRecord` reaches its `createdAt` / `updatedAt` through an @@ -103,18 +112,29 @@ function canonicalIsoInstant(value: unknown): string | undefined { * * [#14037] `rowToRecord` and the two history adapters below each assert a * `string` over a driver row (`row.created_at as string | undefined`, and so - * on). On Postgres and MySQL that assertion is false: `SqlDriver#formatOutput` - * repairs the BUILTIN audit columns (`repairNaiveUtcAuditTimestamp`) and folds - * declared `Field.datetime` columns (`normalizeSqliteDatetimeOutput`) only - * inside its `if (this.isSqlite)` arm, and `withPostgresCalendarDayAsText` - * leaves `timestamptz` / `timestamp` deliberately untouched. Both column - * classes therefore arrive as a JS `Date` on the live dialects — pinned in - * `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`. + * on). On Postgres and MySQL that assertion USED to be false for both column + * classes: `SqlDriver#formatOutput` repaired the BUILTIN audit columns + * (`repairNaiveUtcAuditTimestamp`) and folded declared `Field.datetime` columns + * (`normalizeSqliteDatetimeOutput`) only inside its `if (this.isSqlite)` arm, + * so both arrived as a JS `Date` on the live dialects. #13973 ([ADR-0053 D-F1]) + * lifted both passes out of that gate; they run on EVERY dialect now, and the + * pin that recorded the asymmetry records the canonical-text contract instead + * (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts` + * §B, inverted on purpose). + * + * ⚠️ `withPostgresCalendarDayAsText` is UNCHANGED by that ruling and still + * leaves `timestamptz` / `timestamp` deliberately untouched ([ADR-0053 D-F2]): + * the client library still materialises those columns as a `Date`. What moved + * is where it is folded — at the driver's own read boundary, not at the parser + * — so what reaches this adapter is the canonical text. + * * `MetadataRecord.createdAt` / `.updatedAt` and * `MetadataHistoryRecord.recordedAt` are declared `z.string().datetime()` * (`packages/spec/src/system/metadata-persistence.zod.ts`) — a refinement a * `Date` fails outright. The cast is an assertion about a driver row, never a - * measurement of one, which is why tsc reports nothing. + * measurement of one, which is why tsc reports nothing — and the `Date` domain + * did not close: `driver-sql` hands an INVALID `Date` through unchanged + * ([ADR-0053 D-F3]) and non-SQL drivers materialise their own. * * ⚠️ Deliberately NOT {@link canonicalIsoInstant} above. That difference used * to be exactly one input shape — the Invalid `Date` on which that spelling diff --git a/packages/rest/src/import-job-dto-timestamp-canonical.test.ts b/packages/rest/src/import-job-dto-timestamp-canonical.test.ts index 95b99af12a..9925218698 100644 --- a/packages/rest/src/import-job-dto-timestamp-canonical.test.ts +++ b/packages/rest/src/import-job-dto-timestamp-canonical.test.ts @@ -17,12 +17,22 @@ * milliseconds dropped, the SERVER's timezone baked in, no `Z`, and not * `Date.parse`-safe for a client doing strict ISO parsing. * - * Why all four, and why nothing upstream repaired them: `formatOutput`'s two - * timestamp repairs — the `AUDIT_TIMESTAMP_COLUMNS` pass (`created_at`) and the - * `normalizeSqliteDatetimeOutput` pass over `datetimeFields` - * (`started_at` / `completed_at` / `reverted_at`, all declared `Field.datetime` - * on `sys_import_job`) — both sit INSIDE `formatOutput`'s `if (this.isSqlite)` - * arm. ⚠️ A declared `Field.datetime` is NOT protected on Postgres/MySQL. + * Why all four, and why nothing upstream repaired them AT THE TIME: + * `formatOutput`'s two timestamp repairs — the `AUDIT_TIMESTAMP_COLUMNS` pass + * (`created_at`) and the `normalizeSqliteDatetimeOutput` pass over + * `datetimeFields` (`started_at` / `completed_at` / `reverted_at`, all declared + * `Field.datetime` on `sys_import_job`) — both sat INSIDE `formatOutput`'s + * `if (this.isSqlite)` arm, so a declared `Field.datetime` was NOT protected on + * Postgres/MySQL. #13973 ([ADR-0053 D-F1]) has since lifted both passes out of + * that gate: they run on EVERY dialect, so `driver-sql` no longer hands this + * seam a valid `Date` on any dialect. + * + * ⚠️ That does not make these cases redundant, and the `Date` arm they pin is + * not dead code. `driver-mongodb` stamps `new Date()` and BSON round-trips it, + * and `driver-sql` still hands an INVALID `Date` through unchanged — the one + * shape with no canonical text to fold to ([ADR-0053 D-F3]), measured reachable + * on both live dialects. What these cases own is the mapper's behaviour per + * INPUT SHAPE, which is the property that outlives the dialect fact. * * ## Why the obvious pin would have proved nothing * @@ -57,11 +67,14 @@ * * ## What is deliberately NOT claimed here * - * That `driver-sql` hands this seam a `Date` on Postgres. That is a fact about - * `driver-sql`, measured beside the fix (`formatOutput`'s `isSqlite` bracketing) - * and pinned in that package; `@objectstack/rest` must not grow a Postgres - * dependency to restate it. What these tests own is the mapper's behaviour - * GIVEN each input shape a driver can produce. + * Which shapes `driver-sql` hands this seam, on which dialect. That is a fact + * about `driver-sql` — today the canonical text on every dialect, with the + * Invalid `Date` passing through ([ADR-0053 D-F1]/[D-F3]) — measured and pinned + * in that package (`sql-driver-13973-canonical-iso-read-door.test.ts`); + * `@objectstack/rest` must not grow a Postgres dependency to restate it, and + * must not re-derive it either, which is why nothing here asserts it. What + * these tests own is the mapper's behaviour GIVEN each input shape a driver can + * produce. */ import { describe, it, expect, afterEach } from 'vitest'; diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index b2c85f4adb..c1eb345534 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -691,17 +691,25 @@ function importJobUndoable(row: any): boolean { * [#13994] The input domain is what a DRIVER materialises into such a column, * and it is dialect-dependent — measured, not guessed: * - * - **JS `Date`** — `driver-sql` on Postgres and MySQL. `timestamptz` / - * `DATETIME(3)` are instants and the driver materialises them as `Date` on - * purpose (`SqlDriver.withPostgresCalendarDayAsText` says so in as many - * words); `driver-mongodb` stamps `new Date()` and BSON round-trips it. - * `formatOutput`'s two timestamp repairs — the `AUDIT_TIMESTAMP_COLUMNS` - * pass and the `normalizeSqliteDatetimeOutput` pass over `datetimeFields` — - * both sit INSIDE its `if (this.isSqlite)` arm, so neither runs here. ⚠️ A - * declared `Field.datetime` is therefore NOT protected on Postgres/MySQL. - * - **`string`, already canonical ISO-8601 UTC** — `driver-sql` on SQLite and - * its `driver-turso` / `driver-sqlite-wasm` siblings, and `driver-memory`. - * Passed through unchanged, so a canonical row is a fixed point. + * - **JS `Date`** — `driver-mongodb` stamps `new Date()` and BSON round-trips + * it. On `driver-sql` the CLIENT layer still materialises `timestamptz` / + * `DATETIME(3)` as a `Date` on purpose — those are instants, and + * `SqlDriver.withPostgresCalendarDayAsText` still leaves the parser alone in + * as many words ([ADR-0053 D-F2]) — but that is no longer what leaves the + * read door. Since #13973 ([ADR-0053 D-F1]) `formatOutput`'s two timestamp + * repairs — the `AUDIT_TIMESTAMP_COLUMNS` pass and the + * `normalizeSqliteDatetimeOutput` pass over `datetimeFields` — both run on + * EVERY dialect, so the driver folds that `Date` at its own read boundary. + * ⚠️ Exactly one `Date` shape still arrives here from `driver-sql`: an + * INVALID `Date`, which has no canonical text to fold to and is handed + * through unchanged by design ([ADR-0053 D-F3], `isoFromValidDate`). That + * residue is what keeps this arm live rather than dead — see the #14078 + * section below, which is the arm that absorbs it. + * - **`string`, already canonical ISO-8601 UTC** — `driver-sql` on every + * dialect (SQLite and its `driver-turso` / `driver-sqlite-wasm` siblings + * have always stored the text; Postgres and MySQL are folded to it at the + * read door), and `driver-memory`. Passed through unchanged, so a canonical + * row is a fixed point. * - **anything else** a host stamps into the column — rendered as before. * * Why this is not `String(v)`: on a `Date`, `String` runs