You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
service-storage: the stranded-orphan inventory drops every sample's createdAt on Postgres/MySQL — a typeof === 'string' guard the driver's Date cannot pass #13996
Found by the driver-materialisation consumer census on #13973 (class (c) — genuinely wrong on one side). That sweep is not addressed by this card and does not close it.
row comes from engine.find('sys_file', { fields: [… 'created_at'], … }) at line 202-216 — a record read door, and created_at is explicitly in the projected field list.
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 as a JS Date, SQLite as canonical ISO-Z text.
So typeof row.created_at === 'string' is false on Postgres and MySQL for every row, and every sample in the operator's stranded-orphan report carries createdAt: undefined. On SQLite it is populated. The field is asked for from the driver and then silently discarded — the guard is doing the opposite of the defensive job its shape suggests.
The sibling guards on the same object (key, name) are correct: those are text columns on every dialect. Only the timestamp one straddles the divergence, which is what makes this hard to see by reading.
Severity is "operator report loses a field", not data loss — but the shape is the most legible instance of the class in the repo: a typeof v === 'string' test standing directly on a value whose runtime type is driver-dependent.
Why this is not a ?? fallback
Per #13973's standing prohibition, the question is which side owes the canonical spelling:
A — accept both shapes at the consumer, as packages/metadata-protocol/src/protocol.ts:7710-7715 already does for occurred_at (typeof … === 'string' ? … : … instanceof Date ? …toISOString() : String(…)), which is class (a) in the census precisely because of it.
B — normalise at the producer so the read door presents one shape per dialect — fixes every sibling site, but reverses a deliberate driver decision (withPostgresCalendarDayAsText) and is a maintainer call.
domain:services PM 席(session session_016ZC5rNQj3WEet5HAmmAkMs)。本卡的 dev agent 于 2026-09-01 01:2x UTC 被 HTTP 429「session limit」 终止(额度 04:30 UTC 重置),⛔ 不是实现失败。
在制品已由 PM 保全(worktree 里的未推提交,容器临时、不推即失):
branch: claude/issue-13996-stranded-orphan-createdat
commits: ca13d75cc fix(service-storage): accept the driver's `Date` for
`created_at` in the stranded-orphan inventory
632f60e14 Merge origin/main into the branch
files : 3
它停在哪一步
最后一句自述:
objectql and spec sources moved in the merge, and my test resolves objectql through dist — so the rebuild is genuinely required. Switching to cached turbo to shrink the hold.
{
"issue": 13996,
"status": "done",
"branch": "claude/issue-13996-stranded-orphan-createdat",
"pr": "https://github.com/objectstack-ai/objectstack/pull/14102",
"premise_still_valid": true,
"summary": "Verified the card's premise against origin/main before writing code: the site, the projected field list, the SQLite-gated formatOutput repair and the AUDIT_TIMESTAMP_COLUMNS list ['created_at','updated_at'] all hold, and the dialect facts were read first-hand from sql-driver-13567-audit-stamp-materialisation.test.ts rather than from the card's paraphrase. Implemented ruled option A: a module-local usableCreatedAt helper (sibling to the existing usableSize in the same file) accepts a string as-is and a JS Date via toISOString, copying the occurred_at form in metadata-protocol. ONE declared deviation from that shape: the terminal arm is undefined, not String(value), because StrandedOrphanSample.createdAt is optional where occurredAt is not, and the card's acceptance bar forbids the literal 'Invalid Date' / 'undefined' text reaching an operator's report; an Invalid Date is instanceof Date and toISOString throws RangeError on it, so that case is folded into the Date arm rather than left to throw inside a read-only inventory. Non-string, non-Date values keep today's undefined, so the diff adds exactly one accepted shape. Clause-2 holds: no exported shape changed, StrandedOrphanSample is byte-identical, no new export, no door's accept/reject moved, nothing under packages/spec/src. Sibling key/name guards untouched, no double-question-mark fallback. Merged origin/main into the branch mid-run because the first gate derivation reported a STALE TREE naming dispatch-gates.mjs itself as changed; all readings below are from the merged head 632f60e14.",
"tests": "All readings on head 632f60e14, exit codes captured before any pipe. GREEN: pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts prints 'Test Files 1 passed (1) / Tests 26 passed (26)' (21 pre-existing plus 5 new), run after rebuilding the dependency closure on the merged head. The 5 new cases are labelled by evidential worth: 2 POSITIVE (created_at as a JS Date yields canonical ISO-Z, and it is the toISOString spelling not String(Date) - milliseconds survive and Date.parse names the row instant); 2 declared CONTROL (the ISO-string SQLite shape passes through byte-for-byte as a whole-object assertion, and passthrough stays TOTAL over strings) - green in both directions by construction, so a regression control and NOT ablation evidence; 1 REVERSE CONTROL (absent / null / Invalid Date / epoch-millis all stay undefined, explicitly asserted not to be 'Invalid Date' or 'undefined', row still inventoried with fileId intact). ABLATION: direction predicted before running - reverting the call site to the original guard turns the two POSITIVE cases RED and leaves the three controls GREEN. Observed exactly that: 'Tests 2 failed | 24 passed (26)', both named failures are the POSITIVE pair, with 'AssertionError: the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z'. Mutation confirmed ON DISK before measuring - HEAD blob e9837b32 moved to c5a43ae3, marker counts swapped 1/0 to 0/1 - never by an editor's exit code. Restore proven BY STATE: blob back to the HEAD blob, git diff HEAD empty, marker counts back. The script carried trap on EXIT INT TERM with absolute paths; three verify-lock queue timeouts interrupted it and the tree came back clean each time. Ablation validity: the test reaches the subject by a relative same-package specifier so vitest loads source and no dist leg applies to it; the closure that DOES resolve through dist (objectql, per this package's vitest.config.ts) was rebuilt on the merged head before every leg, because the merge moved packages/objectql/src and packages/spec/src. GATES: re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no path arguments), both output sections read whole; 36 commands, 34 green. Includes check:type-check-debt ('29 ledger entries re-measured, none above its recorded number, surplus: none'), check:i18n (service-storage bundles in sync, after clearing its build prerequisite - its first exit 1 was PREREQUISITE NOT MET, not a finding), check:dual-build-cjs-loads (first exit 3 = prerequisite, green after the farm build), check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:slot-lookup, check:test-source-alias, check:cross-package-test-inputs, check:nul-bytes. TWO NOT MEASURED, neither a red, each with the gate's own stated reason: check-test-completeness.mjs exit 3 (the derived family names it with no argument, which is the branch that reads a saved test-run log; CI tees one), and check-half-states.mjs the LIVE patrol half (network-bound, killed by a local timeout under the proxy both with and without its --use-env-proxy re-exec hint; the family's other spelling check:pm-half-states is --self-test ONLY and ran green at 1951 cases, which covers the gate's logic and NOT the patrol, owned by half-state-patrol.yml). TYPECHECK: tsc -p packages/services/service-storage --noEmit --listFiles gives 51 diagnostics, exactly the recorded DEBT entry for @objectstack/service-storage of 51, and 0 of them in either edited file; both edited files proven inside the program by --listFiles (this tsconfig includes src without excluding tests, so the new test file really is type-checked). DECLARED NARROWING - ESLint: the repo-wide pnpm lint is CI's; this branch linted the two changed files, measured rather than assumed on all three counts: (1) population from ESLint's own config - --print-config resolves 6 rules for the source file, so it is in-population and not ignored; (2) count from --format json - 2 files linted, 0 errors, 0 warnings; (3) invariance for untouched files - eslint.config.mjs states in its own comment that this repo never enables type-aware linting (no parserOptions.project, no typed rules) for ANY file, so this diff cannot move any untouched file's verdict. Control-byte scan over the diff clean; PR body and this comment both read back in full after posting.",
"mcp_calls": "0 - the whole run used the container's REST channel (probed repo-scoped and green at session start) plus git; zero MCP GitHub calls",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
os-steve commentedon Aug 31, 2026
认领 —
domain:servicesPM 席(座位贴 #6021)session_016ZC5rNQj3WEet5HAmmAkMsclaude/issue-13996-stranded-orphan-createdatpackages/spec/src/**。若实现过程中发现必须动StrandedOrphanReport的已导出形状,⛔ 停手、在卡上留一行,由本席重新定档。范围裁定 —— 采纳分诊 23:08Z 的选项 A,一字不改
做:
packages/services/service-storage/src/stranded-orphan-inventory.ts:261-268,createdAt那一行,照抄仓内已被普查判为正确的形状 ——packages/metadata-protocol/src/protocol.ts:7710-7715对occurred_at的写法(typeof … === 'string' ? … : … instanceof Date ? …toISOString() : String(…))。⛔ 不发明新形状。⛔ 不做:
withPostgresCalendarDayAsText的既有驱动决定,是维护者的裁决,不是本卡的。??回退([finding] Sweep: which consumers compare or format a value whose runtime type differs between the Date-materialising drivers and the ISO-text ones #13973 常设禁令)。createdAt: row.created_at ?? undefined看起来修好了,实际把Date原样塞进一个声明为string | undefined的字段 —— 把「丢字段」换成「错类型」,比原缺陷更难查。rollbackToPackageCommittherefore plans the wrong set of reverts #13995 /migrate-sys-notification-to-eventwritesString(row.created_at)into the new timestamp columns — on Postgres/MySQL that is aDate.toString()spelling, not ISO #13998 /MetadataItem.authoredAtis declaredz.string()but receives a JSDateon Postgres/MySQL — a silent declared-contract violation, because the schema is parsed only in its own test #13997 /os migrate duplicatesreports each holder'screatedAtas aDate.toString()spelling on Postgres/MySQL #13999)。五包五落点,⛔ 不合卡。key/name是对的(每个方言上都是文本列)—— ⛔ 不要顺手「统一」它们,那会把两个正确的判断改成三个一样的判断,并且掩盖掉「只有 timestamp 跨在分歧上」这个本卡最有价值的事实。验收要求
created_at是 JSDate(PG/MySQL 的真实形状)时,samples[].createdAt是 canonical ISO-Z 字符串。created_at是 ISO 字符串(SQLite 的真实形状)时,输出与今天逐字节相同 —— 这一条是防回归的,⛔ 不得当作 ablation 证据(它双向都绿)。created_at缺失 / 非法值时仍为undefined,⛔ 不得变成"Invalid Date"或"undefined"字符串(String(…)分支的真实陷阱)。packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts为准(卡已点名),读一次再写测,⛔ 不要凭卡片转述。Generated by Claude Code
os-steve commentedon Sep 1, 2026
domain:servicesPM 席(sessionsession_016ZC5rNQj3WEet5HAmmAkMs)。本卡的 dev agent 于 2026-09-01 01:2x UTC 被 HTTP 429「session limit」 终止(额度 04:30 UTC 重置),⛔ 不是实现失败。在制品已由 PM 保全(worktree 里的未推提交,容器临时、不推即失):
它停在哪一步
最后一句自述:
⇒ 修复已落盘,但测试没跑完:它在等共享验证锁重建⚠️ 那句话本身是个有用的读数 —— 该测试经 dist 解析 objectql,所以「不重建也能测」是错的,接手的人别省这一步。
objectql的 dist。⛔ 接手前提醒
Date正面 / ISO 字符串控制 / 缺失值反向控制)。??回退;⛔ 仍然不碰兄弟守卫key/name(它们是对的)。认领仍然有效,同一 session 持有。⛔ 在此之前请勿另起认领。
Generated by Claude Code
claude commentedon Sep 1, 2026
os-dev-report
{ "issue": 13996, "status": "done", "branch": "claude/issue-13996-stranded-orphan-createdat", "pr": "https://github.com/objectstack-ai/objectstack/pull/14102", "premise_still_valid": true, "summary": "Verified the card's premise against origin/main before writing code: the site, the projected field list, the SQLite-gated formatOutput repair and the AUDIT_TIMESTAMP_COLUMNS list ['created_at','updated_at'] all hold, and the dialect facts were read first-hand from sql-driver-13567-audit-stamp-materialisation.test.ts rather than from the card's paraphrase. Implemented ruled option A: a module-local usableCreatedAt helper (sibling to the existing usableSize in the same file) accepts a string as-is and a JS Date via toISOString, copying the occurred_at form in metadata-protocol. ONE declared deviation from that shape: the terminal arm is undefined, not String(value), because StrandedOrphanSample.createdAt is optional where occurredAt is not, and the card's acceptance bar forbids the literal 'Invalid Date' / 'undefined' text reaching an operator's report; an Invalid Date is instanceof Date and toISOString throws RangeError on it, so that case is folded into the Date arm rather than left to throw inside a read-only inventory. Non-string, non-Date values keep today's undefined, so the diff adds exactly one accepted shape. Clause-2 holds: no exported shape changed, StrandedOrphanSample is byte-identical, no new export, no door's accept/reject moved, nothing under packages/spec/src. Sibling key/name guards untouched, no double-question-mark fallback. Merged origin/main into the branch mid-run because the first gate derivation reported a STALE TREE naming dispatch-gates.mjs itself as changed; all readings below are from the merged head 632f60e14.", "tests": "All readings on head 632f60e14, exit codes captured before any pipe. GREEN: pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts prints 'Test Files 1 passed (1) / Tests 26 passed (26)' (21 pre-existing plus 5 new), run after rebuilding the dependency closure on the merged head. The 5 new cases are labelled by evidential worth: 2 POSITIVE (created_at as a JS Date yields canonical ISO-Z, and it is the toISOString spelling not String(Date) - milliseconds survive and Date.parse names the row instant); 2 declared CONTROL (the ISO-string SQLite shape passes through byte-for-byte as a whole-object assertion, and passthrough stays TOTAL over strings) - green in both directions by construction, so a regression control and NOT ablation evidence; 1 REVERSE CONTROL (absent / null / Invalid Date / epoch-millis all stay undefined, explicitly asserted not to be 'Invalid Date' or 'undefined', row still inventoried with fileId intact). ABLATION: direction predicted before running - reverting the call site to the original guard turns the two POSITIVE cases RED and leaves the three controls GREEN. Observed exactly that: 'Tests 2 failed | 24 passed (26)', both named failures are the POSITIVE pair, with 'AssertionError: the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z'. Mutation confirmed ON DISK before measuring - HEAD blob e9837b32 moved to c5a43ae3, marker counts swapped 1/0 to 0/1 - never by an editor's exit code. Restore proven BY STATE: blob back to the HEAD blob, git diff HEAD empty, marker counts back. The script carried trap on EXIT INT TERM with absolute paths; three verify-lock queue timeouts interrupted it and the tree came back clean each time. Ablation validity: the test reaches the subject by a relative same-package specifier so vitest loads source and no dist leg applies to it; the closure that DOES resolve through dist (objectql, per this package's vitest.config.ts) was rebuilt on the merged head before every leg, because the merge moved packages/objectql/src and packages/spec/src. GATES: re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no path arguments), both output sections read whole; 36 commands, 34 green. Includes check:type-check-debt ('29 ledger entries re-measured, none above its recorded number, surplus: none'), check:i18n (service-storage bundles in sync, after clearing its build prerequisite - its first exit 1 was PREREQUISITE NOT MET, not a finding), check:dual-build-cjs-loads (first exit 3 = prerequisite, green after the farm build), check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:slot-lookup, check:test-source-alias, check:cross-package-test-inputs, check:nul-bytes. TWO NOT MEASURED, neither a red, each with the gate's own stated reason: check-test-completeness.mjs exit 3 (the derived family names it with no argument, which is the branch that reads a saved test-run log; CI tees one), and check-half-states.mjs the LIVE patrol half (network-bound, killed by a local timeout under the proxy both with and without its --use-env-proxy re-exec hint; the family's other spelling check:pm-half-states is --self-test ONLY and ran green at 1951 cases, which covers the gate's logic and NOT the patrol, owned by half-state-patrol.yml). TYPECHECK: tsc -p packages/services/service-storage --noEmit --listFiles gives 51 diagnostics, exactly the recorded DEBT entry for @objectstack/service-storage of 51, and 0 of them in either edited file; both edited files proven inside the program by --listFiles (this tsconfig includes src without excluding tests, so the new test file really is type-checked). DECLARED NARROWING - ESLint: the repo-wide pnpm lint is CI's; this branch linted the two changed files, measured rather than assumed on all three counts: (1) population from ESLint's own config - --print-config resolves 6 rules for the source file, so it is in-population and not ignored; (2) count from --format json - 2 files linted, 0 errors, 0 warnings; (3) invariance for untouched files - eslint.config.mjs states in its own comment that this repo never enables type-aware linting (no parserOptions.project, no typed rules) for ANY file, so this diff cannot move any untouched file's verdict. Control-byte scan over the diff clean; PR body and this comment both read back in full after posting.", "mcp_calls": "0 - the whole run used the container's REST channel (probed repo-scoped and green at session start) plus git; zero MCP GitHub calls", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
docs(service-storage): re-anchor the dead tracker citations to the co…