Skip to content

metadata-protocol: listCommits declares createdAt as a string but emits the raw driver value, so it hands consumers a Date on Postgres/MySQL #14038

Description

@claude

Found while implementing #13995 (which is not this, and does not close this). Same class as the #13973 driver-materialisation census — class (c), genuinely wrong on one side — but a site the census's seven do not include: #13997 covers MetadataItem.authoredAt in sys-metadata-repository.ts and MetadataStats.mtime in database-loader.ts, and neither is this one.

The site

packages/metadata-protocol/src/protocol.ts, listCommits. Its declared return type says:

    }>): Promise(Array({
        ...
        createdAt?: string;
    }))

(spelled loosely above to keep angle brackets out of this body; the real declaration is the Promise of an Array of that object literal, and the member is verbatim createdAt?: string.)

The mapping assigns the RAW driver value:

const mapped = rows.map((r) => ({
  ...
  ...(r.created_at ? { createdAt: r.created_at } : {}),
}));

Why it is wrong on the production default driver

created_at is a builtin audit column: not in datetimeFields, and SqlDriver#formatOutput repairs it only inside if (this.isSqlite). Pinned in packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts — the live dialects hand it out of the record read door as a JS Date, SQLite as canonical ISO-Z text.

So on Postgres and MySQL, listCommits hands every in-process consumer a Date in a field the type says is a string, on every call, silently.

Why nothing reports it

  • The type system cannot see it. rows is any[], so the assignment is unchecked; every consumer then reads a string that is really a Date, and TypeScript agrees with the declaration rather than with the value.
  • The REST door hides it. GET /packages/:id/commits returns through deps.success({ commits }), and JSON.stringify renders a Date as canonical ISO-Z text — so the WIRE shape is accidentally correct and no HTTP-level test can catch this. Only in-process consumers see the Date.
  • Every test drives SQLite or memory, where the value really is an ISO string.

That combination is exactly what let #13995 exist: the in-process consumer that read this field and compared it as a string was rollbackToPackageCommit, and the declaration told everyone reading the code that a string was what arrived.

Why this is not repaired inside #13995

#13995's ruled route is "compare instants at the consumer", and it is deliberately minimal: it changes the two COMPARISONS and nothing about what listCommits emits. Repairing this one means choosing what the emitted value should be, and that is a judgement call with a blast radius, not a mechanical edit:

⛔ Unassigned. Filed for triage to grade and route; it is one of the shapes #13973's option B would delete outright, so it may be worth holding against that ruling.


Generated by Claude Code

Activity

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