Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .changeset/isqlite-comment-drift-sweep-round-two.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
'@objectstack/cli': patch
---

Correct the remaining 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 the
record read door presents the builtin audit columns and every declared `Field.datetime`
as canonical ISO-8601-`Z` text on Postgres and MySQL as well as SQLite. Measured on the
tree rather than recalled: in `packages/drivers/driver-sql/src/sql-driver.ts` the
`if (this.isSqlite)` arm inside `formatOutput` opens at line 16965 and closes at 17026,
covering only the JSON codec and the numeric-scalar repair, while the audit-column loop
(17046) and the `normalizeSqliteDatetimeOutput` loop (17061) both sit at the method's top
level, below that closing brace.

Two of the corrected comments were load-bearing rather than merely stale. The
`service-storage` one drew a conclusion for a live read door from the false premise, and
it also claimed that folding at the driver's read boundary "would reverse the deliberate
`withPostgresCalendarDayAsText` decision" — which is what #13973 ruled and did. The two
`packages/cli` ones attached the wrong reason to a true fact: the holder probe reads
through the raw-SQL seam, so `formatOutput` never runs on that path at all, and the
dialect divergence there survives the ruling for that reason and not because of a gate.

Comments only — no runtime behaviour, no exported symbol and no public type changes.
`@objectstack/cli` is the one package named here because its per-file build carries the
amended text verbatim into `dist/commands/migrate/duplicates.js` and
`dist/commands/migrate/duplicates.d.ts`, so its published output changes.
`@objectstack/metadata-protocol` is deliberately NOT named: its edits are all in test
files, which are not published. `@objectstack/service-storage` and `@objectstack/metadata`
are deliberately NOT named either: their source edits are JSDoc blocks on the internal
`usableCreatedAt` and `canonicalTimestampText`, and both bundles strip them — measured
absent from `dist/`, with each package's identifier found in the same `dist/` (and the
exported `StrandedOrphanInventoryEngine` docblock present in `dist/index.d.ts`) as the
firing control that the probe works.

Three 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 Invalid `Date` residue still stands (D-F3 — the one `Date`
shape with no canonical text leaves the read door unchanged, so no sentence claims the
read door never hands out a `Date`); and the ruled-B consumer arms stay, with only the
prose explaining why they exist corrected.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
*
* `DuplicateHolder.createdAt` is declared `string | null`, and the mapper built
* it with `String(row.created_at)`. `created_at` is a BUILTIN audit column — not
* in `datetimeFields`, and `SqlDriver#formatOutput` repairs it only inside its
* `if (this.isSqlite)` arm — and the holder probe reads through the raw-SQL seam,
* so no presentation runs on this path at all. The dialect therefore decides what
* arrives:
* in `datetimeFields`, so no declared-field coercion reaches it — and the holder
* probe reads through the raw-SQL seam, so `formatOutput` does not run on this
* path at all. The dialect therefore decides what arrives:
*
* - **Postgres / MySQL** materialise a JS `Date`, so `String()` ran
* `Date.prototype.toString`: `Sun Aug 30 2026 18:19:25 GMT+0800 (China
Expand Down Expand Up @@ -43,8 +42,18 @@
* expression really produced.
*
* ⛔ Not a `??` fallback and not a driver change: `withPostgresCalendarDayAsText`
* is a deliberate driver decision and is untouched. The CLI is a leaf consumer
* with a declared `string | null`, so the canonical spelling is owed here.
* is a deliberate driver decision and is untouched ([ADR-0053 D-F2]). The CLI is
* a leaf consumer with a declared `string | null`, so the canonical spelling is
* owed here.
*
* ⚠️ What keeps this file live is the SEAM, ⛔ not an `if (this.isSqlite)` gate
* inside `formatOutput`. That gate is gone: #13973 ([ADR-0053 D-F1]) lifted both
* of `formatOutput`'s timestamp passes — the `AUDIT_TIMESTAMP_COLUMNS` pass and
* the `normalizeSqliteDatetimeOutput` pass over `datetimeFields` — out of it and
* they run on EVERY dialect, so the RECORD read door presents the canonical text
* everywhere. The holder probe does not go through that door; §B3 of the pin
* named above reads the same row raw through knex and still gets the dialect's
* `Date` on the live cells, which is the fact §A1 drives.
*/

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
Expand Down
25 changes: 17 additions & 8 deletions packages/cli/src/commands/migrate/duplicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,14 +578,23 @@ export function answeringSeam(exec: SeedTenancyExec): SeedTenancyExec {
/**
* The canonical `createdAt` spelling for one holder row.
*
* `created_at` is a BUILTIN audit column: it is not in `datetimeFields`, and
* `SqlDriver#formatOutput` repairs it only inside its `if (this.isSqlite)` arm —
* and the holder probe reads through the raw-SQL seam anyway, so no presentation
* runs on this path at all. The DIALECT therefore decides what lands in
* `row.created_at`, and both sides of that asymmetry are pinned in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`:
* Postgres and MySQL materialise a JS `Date`, SQLite and its siblings hand back
* canonical ISO-8601 UTC text.
* `created_at` is a BUILTIN audit column: it is not in `datetimeFields`, so no
* declared-field coercion reaches it — and the holder probe reads through the
* raw-SQL seam anyway, so `formatOutput` does not run on this path at all. The
* DIALECT therefore decides what lands in `row.created_at`: Postgres and MySQL
* materialise a JS `Date`, SQLite and its siblings hand back canonical ISO-8601
* UTC text. Pinned in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B3, which reads the same row raw through knex and still gets the dialect's
* `Date` on the live cells.
*
* ⚠️ The reason is the SEAM, ⛔ not an `if (this.isSqlite)` gate inside
* `formatOutput`. That gate is gone: #13973 ([ADR-0053 D-F1]) lifted both of
* `formatOutput`'s timestamp passes — the `AUDIT_TIMESTAMP_COLUMNS` pass and
* the `normalizeSqliteDatetimeOutput` pass over `datetimeFields` — out of it,
* and they run on EVERY dialect now, so the record read door presents the
* canonical text everywhere. This path simply never reaches that door, which is
* why the divergence survives HERE after the driver closed it there.
*
* `DuplicateHolder.createdAt` is declared `string | null`, so this leaf consumer
* is the side that owes the canonical spelling — the same form the `occurredAt`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,27 @@
* ## The defect
*
* `created_at` is an engine-injected audit column: it is not in
* `datetimeFields`, and `SqlDriver#formatOutput` repairs it (both the
* builtin-audit-column repair and the `datetimeFields` fold) only inside its
* `if (this.isSqlite)` arm (`sql-driver.ts`, `formatOutput`). Postgres and
* MySQL therefore hand this column out of the record read door as a JS
* `Date`, while the SQLite family hands out canonical ISO-Z text — pinned
* live in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* So on the production default driver, `listCommits` handed every
* in-process consumer a `Date` in a field the type says is a `string`.
* `datetimeFields`, and when this landed `SqlDriver#formatOutput` repaired it
* (both the builtin-audit-column repair and the `datetimeFields` fold) only
* inside its `if (this.isSqlite)` arm (`sql-driver.ts`, `formatOutput`).
* Postgres and MySQL therefore handed this column out of the record read door
* as a JS `Date`, while the SQLite family handed out canonical ISO-Z text. So
* on the production default driver, `listCommits` handed every in-process
* consumer a `Date` in a field the type says is a `string`.
*
* #13973 ([ADR-0053 D-F1]) has since lifted BOTH passes out of that gate — they
* run on EVERY dialect now, so the record read door presents the canonical
* text — and the pin that recorded the asymmetry records that contract instead
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B1, inverted on purpose).
*
* ⚠️ That does not make the cases below historical. `withPostgresCalendarDayAsText`
* is untouched by that ruling ([ADR-0053 D-F2]) — the CLIENT still materialises
* `timestamptz` / `DATETIME(3)` as a `Date`, and what moved is where the driver
* folds it — and the `Date` domain at this mapper did not close: `driver-sql`
* hands an INVALID `Date` through unchanged ([ADR-0053 D-F3]) and non-SQL
* drivers materialise their own. What these cases own is the mapper's behaviour
* per INPUT SHAPE, which outlives the dialect fact.
*
* ## Why the fixture drives a hand-made `Date`
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,20 @@
// The defect
// ---------------------------------------------------------------------------
// `created_at` is an engine-injected audit column: it is not in `datetimeFields`
// and `SqlDriver#formatOutput` repairs it only inside `if (this.isSqlite)`, so
// the live SQL dialects hand it out of the record read door as a JS `Date` while
// the SQLite family hands out canonical ISO-Z text. Pinned one layer down by
// `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
// and, when this landed, `SqlDriver#formatOutput` repaired it only inside
// `if (this.isSqlite)`, so the live SQL dialects handed it out of the record
// read door as a JS `Date` while the SQLite family handed out canonical ISO-Z
// text. #13973 ([ADR-0053 D-F1]) has since lifted both of `formatOutput`'s
// timestamp passes out of that gate — they run on EVERY dialect now — and the
// pin one layer down records that contract instead
// (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
// §B1, inverted on purpose).
//
// The `Date` this file drives is still a shape `compareAuditInstants` receives,
// so what is pinned below stays a live comparator arm: `withPostgresCalendarDayAsText`
// is untouched by that ruling ([ADR-0053 D-F2]) so the CLIENT still materialises
// the column as a `Date`, `driver-sql` hands an INVALID `Date` through unchanged
// ([ADR-0053 D-F3]), and non-SQL drivers materialise their own.
//
// Both timeline consumers in `protocol.ts` compared `String(created_at)`:
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,22 @@
* - `rowToItem()` (reached by `get()`) — `updated_at` / `created_at`, the
* BUILTIN audit columns.
*
* On Postgres and MySQL both arrive out of the record read door as a JS
* `Date`: `SqlDriver#formatOutput` repairs the audit columns and folds
* declared `datetime` columns 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`.
* When this landed, both arrived out of the record read door as a JS `Date` on
* Postgres and MySQL: `SqlDriver#formatOutput` repaired the audit columns and
* folded declared `datetime` columns only inside its `if (this.isSqlite)` arm.
* #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 records the
* canonical-text contract instead
* (`packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B1, inverted on purpose).
*
* ⚠️ `withPostgresCalendarDayAsText` is untouched by that ruling
* ([ADR-0053 D-F2]) and still leaves `timestamptz` / `timestamp` deliberately
* alone — the CLIENT still hands back a `Date`; what changed is that the driver
* folds it at its own read boundary. The `Date` this file plants therefore
* still reaches both adapters: `driver-sql` hands an INVALID `Date` through
* unchanged ([ADR-0053 D-F3]) and non-SQL drivers materialise their own, so
* what is pinned below is a live adapter arm rather than a historical one.
*
* ## Why nothing reported it, and what that costs THIS file
*
Expand Down
18 changes: 14 additions & 4 deletions packages/metadata/src/loaders/database-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1514,10 +1514,20 @@ describe('MetadataManager auto-configuration', () => {
* (`packages/spec/src/system/metadata-persistence.zod.ts`) — stricter than the
* sibling `MetadataItem.authoredAt`, which is a bare `z.string()`. `stat()`
* built it from `record.updatedAt ?? record.createdAt`, and `created_at` /
* `updated_at` are BUILTIN audit columns: not in `datetimeFields`, and
* `SqlDriver#formatOutput` repairs them only inside its `if (this.isSqlite)`
* arm. On Postgres and MySQL they arrive as a JS `Date`, pinned live in
* `packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts`.
* `updated_at` are BUILTIN audit columns: not in `datetimeFields`, and when
* this landed `SqlDriver#formatOutput` repaired them only inside its
* `if (this.isSqlite)` arm, so on Postgres and MySQL they arrived as a JS
* `Date`. #13973 ([ADR-0053 D-F1]) has since lifted both of `formatOutput`'s
* timestamp passes out of that gate — they run on EVERY dialect — 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`
* §B1, inverted on purpose).
*
* ⚠️ The `Date` these cases drive is still a shape `stat()` receives, so they
* pin a LIVE arm: `withPostgresCalendarDayAsText` is untouched by that ruling
* ([ADR-0053 D-F2]) so the CLIENT still materialises the column as a `Date`,
* `driver-sql` hands an INVALID `Date` through unchanged ([ADR-0053 D-F3]),
* and non-SQL drivers materialise their own.
*
* ⚠️ `rowToRecord` reaches `createdAt` / `updatedAt` through an unchecked
* `row.created_at as string | undefined` cast, so the `string` in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,20 +491,32 @@ async function recordNotificationEventReceipt(
* `formatOutput`, so none of its repairs apply here on any dialect:
*
* - `created_at` is a BUILTIN audit column, so it is never in `datetimeFields`
* and no declared-field coercion reaches it; `formatOutput` repairs it only
* inside its `if (this.isSqlite)` arm (`repairNaiveUtcAuditTimestamp` over
* `AUDIT_TIMESTAMP_COLUMNS`).
* and no declared-field coercion reaches it; what repairs it is
* `formatOutput`'s own `AUDIT_TIMESTAMP_COLUMNS` pass
* (`repairNaiveUtcAuditTimestamp`) at the RECORD read door — a door this
* path does not go through.
* - `read_at` is a LEGACY column ADR-0030 removed from the object, so it is
* not declared either — it can never enter `datetimeFields`, and it is not
* an audit column, so no arm of `formatOutput` could reach it even at the
* record read door.
*
* ⚠️ The reason no repair reaches this path is the SEAM, ⛔ not an
* `if (this.isSqlite)` gate inside `formatOutput`. Both of `formatOutput`'s
* timestamp passes — the `AUDIT_TIMESTAMP_COLUMNS` pass and the
* `normalizeSqliteDatetimeOutput` pass over `datetimeFields` — sat inside that
* arm until #13973 ([ADR-0053 D-F1]) lifted them out, and they run on EVERY
* dialect now. So the record read door presents canonical text everywhere while
* this raw-SQL door still hands back whatever the client materialised, which is
* why the divergence below survives the ruling HERE and nowhere upstream of it.
*
* On SQLite both arrive as canonical ISO text and `String()` is the identity —
* which is why every test in this directory stayed green. On Postgres and
* MySQL an instant column materialises as a JS `Date`
* (`withPostgresCalendarDayAsText` leaves the instant types alone deliberately;
* pinned in `sql-driver-13567-audit-stamp-materialisation.test.ts`), and
* `String(date)` spells
* (`withPostgresCalendarDayAsText` leaves the instant types alone deliberately,
* [ADR-0053 D-F2]; pinned in
* `sql-driver-13567-audit-stamp-materialisation.test.ts` §B3, which reads the
* same row raw through knex and still gets the dialect's `Date` on the live
* cells), and `String(date)` spells
*
* Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,11 +513,23 @@ describe("[#10950] the sweep cannot nominate a stranded orphan — the card's pr
* ## Why this file could not have caught the defect before
*
* `created_at` is a BUILTIN audit column, so no declared-field coercion
* reaches it and `SqlDriver#formatOutput` repairs it only inside its
* `if (this.isSqlite)` arm. The record read door therefore hands it back as
* canonical ISO-Z TEXT on SQLite and as a JS `Date` on Postgres and MySQL —
* pinned at that door, per dialect, in driver-sql's
* `sql-driver-13567-audit-stamp-materialisation.test.ts` (§B0/§B1).
* reaches it and `SqlDriver#formatOutput` USED TO repair it only inside its
* `if (this.isSqlite)` arm. While that gate stood, the record read door handed
* it back as canonical ISO-Z TEXT on SQLite and as a JS `Date` on Postgres and
* MySQL. #13973 ([ADR-0053 D-F1]) has since lifted both of `formatOutput`'s
* timestamp passes out of that gate — they run on EVERY dialect now — and the
* pin that recorded the asymmetry records the canonical-text contract instead
* (driver-sql's `sql-driver-13567-audit-stamp-materialisation.test.ts`
* §B0/§B1, inverted on purpose).
*
* ⚠️ The `Date` these cases drive is still a shape `usableCreatedAt` receives,
* so they pin a LIVE arm rather than a historical one: `driver-sql` hands an
* INVALID `Date` through unchanged ([ADR-0053 D-F3] — the one shape with no
* canonical text) and a non-SQL driver materialises its own.
* `withPostgresCalendarDayAsText` is untouched by that ruling
* ([ADR-0053 D-F2]), so the CLIENT still materialises `timestamptz` /
* `DATETIME(3)` as a `Date`; what changed is that the driver folds it at its
* own read boundary.
*
* ⚠️ This repo's default test backend is SQLite, and every fixture above
* spells `created_at` as an ISO STRING — the one shape the old
Expand Down
Loading
Loading