Uh oh!
There was an error while loading. Please reload this page.
fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102
Conversation
…the stranded-orphan inventory `inventoryStrandedFileOrphans` projects `created_at` out of the `sys_file` read door and then tested it with `typeof row.created_at === 'string'`. `created_at` is a BUILTIN audit column, so no declared-field coercion reaches it and `SqlDriver#formatOutput` repairs the audit columns only inside its `if (this.isSqlite)` arm: that door hands the value back as canonical ISO-Z text on SQLite and as a JS `Date` on Postgres and MySQL, the production default drivers. The guard was therefore false for every row on both live dialects and the projected field was silently discarded from every sample. Accept both shapes at the consumer, in the form `@objectstack/metadata-protocol` already carries for `occurred_at`. Normalising at the driver's read door instead would reverse the deliberate `withPostgresCalendarDayAsText` decision that a `timestamptz` is an instant, so the consumer owes the canonical spelling. The terminal arm stays `undefined` rather than `String(value)`: this field is optional where `occurredAt` is not, and stringifying a null or an Invalid Date would spell `"undefined"` / `"Invalid Date"` into the position an operator reads a timestamp from. The sibling `key` / `name` guards are untouched — those are text columns on every dialect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
…randed-orphan-createdat
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e && git checkout 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4bf15db4c9bdef90b440b1aafd5213b9cee784b5 632f60e146cac151c93e753e2cc48b5d612a49cc && git checkout -B drift-repro 4bf15db4c9bdef90b440b1aafd5213b9cee784b5 && git merge --no-ff 632f60e146cac151c93e753e2cc48b5d612a49cc
node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5 |
os-steve
commented
Sep 1, 2026
PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge落地前检按最终 3 条路径当场重跑: ⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来派发令写的是「照抄
⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。 证据形状,逐条对上
Generated by Claude Code |
Fixes#13996
inventoryStrandedFileOrphansprojectscreated_atout of thesys_filereaddoor and then tested it with
typeof row.created_at === 'string'.created_atis a BUILTIN audit column: it is not indatetimeFields, so nodeclared-field coercion reaches it, and
SqlDriver#formatOutputrepairs the auditcolumns only inside its
if (this.isSqlite)arm. That door therefore hands the valueback as canonical ISO-Z text on SQLite and as a JS
Dateon Postgres and MySQL — theproduction default drivers. The guard was
falsefor every row on both livedialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried
createdAt: undefinedtherewhile looking correct on the SQLite the suite runs on.
Dialect facts taken from
packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts(read directly, not paraphrased): §B1 SQLite cell pins ISO-Z TEXT, §B1 live cell pins
updated_at instanceof Date, and the file's header states the SQLite-gated repair overAUDIT_TIMESTAMP_COLUMNS, which is['created_at', 'updated_at'].The repair
Accept both shapes at the consumer, in the form
@objectstack/metadata-protocolalready carries for
occurred_at(packages/metadata-protocol/src/protocol.ts).Normalising at the driver's read door instead would reverse the deliberate
withPostgresCalendarDayAsTextdecision that atimestamptzIS an instant, so theconsumer owes the canonical spelling.
The accepting logic is a module-local
usableCreatedAt, sibling to the existingusableSizehelper in the same file, so the "why" sits next to the judgement.One declared deviation from the copied shape. The
occurred_atsite ends inString(value)because its field is non-optional.StrandedOrphanSample.createdAtisoptional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefinedand never surface as the literal text "Invalid Date" or "undefined" in theposition an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is
undefinedrather than a coercion. An Invalid Date isinstanceof DateandtoISOString()throwsRangeErroron it, so that case isfolded into the Date arm rather than left to throw inside a read-only inventory.
⛔ Not a
??fallback:row.created_at ?? undefinedreads as fixed and puts a rawDateinto a field declaredstring | undefined, trading a dropped field for a wrongtype. ⛔ The sibling
key/nameguards are untouched — they are correct (textcolumns on every dialect), and only the timestamp straddles the divergence.
No exported shape changed.
StrandedOrphanSampleis byte-identical.Evidence — it must distinguish dialects, because SQLite already worked
Head
632f60e14. Everything below was run on that commit.Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
Five new cases in
stranded-orphan-inventory.test.ts, each labelled with what it isworth as evidence:
created_atas a JSDate(the PG/MySQL shape) yields acanonical ISO-Z string, and it is the
toISOString()spelling rather thanString(Date): the milliseconds survive and the stamp names the row instant.(asserted as the whole sample object), and passthrough stays TOTAL over strings so the
repair cannot quietly start re-parsing the shape that already round-tripped.
⛔ Green in both directions by construction — a regression control, not ablation
evidence.
null, Invalid Date and epoch-millis all stayundefined, explicitly asserted not to be "Invalid Date" or "undefined", with therow still inventoried and its
fileIdintact.Ablation. Direction predicted before running: reverting the call site to the original
guard turns the two POSITIVE cases RED and leaves the CONTROL and REVERSE CONTROL cases
GREEN. Observed exactly that:
Both failures are the POSITIVE pair, so the three control cases are among the 24 that
passed. Mutation confirmed ON DISK before measuring — HEAD blob
e9837b32moved toc5a43ae3, marker counts swapped 1/0 to 0/1 — never by an editor's exit code. Restoreproven BY STATE: blob back to the HEAD blob,
git diff HEADempty, marker counts back.The mutation script carried
trap ... EXIT INT TERMwith absolute paths throughout; itwas interrupted three times by verify-lock queue timeouts and the tree came back clean
each time.
Ablation validity: the test reaches the module under test by a relative same-package
specifier, so vitest loads the source directly and no
dist/leg applies to the subject.The dependency closure — which does resolve through
dist, notably@objectstack/objectqlper this package'svitest.config.ts— was rebuilt on themerged head before every leg, because the merge moved
packages/objectql/srcandpackages/spec/src.Gates
Re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackon this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/mainwas merged in first). 36 commands across the path-derived andconvention-triggered families. Exit codes captured before any pipe.
Green: 34 of 36, including
check:type-check-debt(29 ledger entries re-measured, noneabove its recorded number, "surplus: none"),
check:i18n(service-storage bundles insync, after clearing its build prerequisite),
check:dual-build-cjs-loads,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:slot-lookup,check:test-source-alias,check:cross-package-test-inputsandcheck:nul-bytes.NOT MEASURED, with the reason each gate itself gives — neither is a red:
node scripts/check-test-completeness.mjs— exit 3. The derived family names it withno argument, which is the branch that reads a saved test-run log; there is no local log
to hand it. CI tees one on every invocation.
node scripts/pm/check-half-states.mjs— the LIVE patrol half, network-bound; killed bya local timeout under the proxy both with and without the
--use-env-proxyre-exec itsuggests. The family's other spelling,
pnpm check:pm-half-states, is--self-testONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
which
half-state-patrol.ymlowns.Typecheck.
tsc -p packages/services/service-storage --noEmit --listFiles: 51diagnostics, exactly the recorded
DEBT['@objectstack/service-storage']figure of 51,and 0 of them in either edited file. Both edited files proven inside the program by
--listFiles(this package's tsconfig includessrcwithout excluding tests, so thenew test file really is type-checked).
Declared narrowing — ESLint. The repo-wide
pnpm lintrun is CI's; this branch ranthe two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config —
--print-configresolves6 rules for the source file, so it is in-population and not ignored; (2) the count comes
from
--format json: 2 files linted, 0 errors, 0 warnings; (3) the config cannot moveany untouched file's verdict, because
eslint.config.mjsstates in its own comment thatthis repo "never enables type-aware linting (no parserOptions.project, no typed
@typescript-eslint rules) for ANY file".
Scope
Consumer-side guard inside one function. No new export, no change to any door's
accept/reject, nothing under
packages/spec/src. Out of scope and untouched, allstill open: #13995, #13998, #13997, #13999, the #13973 census and the #13382 OCC seam —
five packages, five sites, deliberately not merged into one card.
Generated by Claude Code
Generated by Claude Code