fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(service-storage): report createdAt on stranded-orphan samples across every dialect - #14102

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat
Sep 1, 2026
Merged

fix(service-storage): report createdAt on stranded-orphan samples across every dialect#14102
os-steve merged 2 commits into
mainfrom
claude/issue-13996-stranded-orphan-createdat

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13996

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: it is not in datetimeFields, so no
declared-field coercion reaches it, and SqlDriver#formatOutput repairs the audit
columns only inside its if (this.isSqlite) arm. That door therefore 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 false for every row on both live
dialects: a field explicitly asked for from the driver was silently discarded, and every
sample in an operator's stranded-orphan report carried createdAt: undefined there
while 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 over
AUDIT_TIMESTAMP_COLUMNS, which is ['created_at', 'updated_at'].

The repair

Accept both shapes at the consumer, in the form @objectstack/metadata-protocol
already carries for occurred_at (packages/metadata-protocol/src/protocol.ts).
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 accepting logic is a module-local usableCreatedAt, sibling to the existing
usableSize helper in the same file, so the "why" sits next to the judgement.

One declared deviation from the copied shape. The occurred_at site ends in
String(value) because its field is non-optional. StrandedOrphanSample.createdAt is
optional, and the card's acceptance bar requires that a missing or malformed stamp stay
undefined and never surface as the literal text "Invalid Date" or "undefined" in the
position an operator reads a timestamp from. So the two-arm accept is copied verbatim in
substance and the terminal arm is undefined rather than a coercion. 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.

⛔ Not a ?? fallback: row.created_at ?? undefined reads as fixed and puts a raw
Date into a field declared string | undefined, trading a dropped field for a wrong
type. ⛔ The sibling key / name guards are untouched — they are correct (text
columns on every dialect), and only the timestamp straddles the divergence.

No exported shape changed. StrandedOrphanSample is 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 is
worth as evidence:

  • POSITIVE (x2) — created_at as a JS Date (the PG/MySQL shape) yields a
    canonical ISO-Z string, and it is the toISOString() spelling rather than
    String(Date): the milliseconds survive and the stamp names the row instant.
  • CONTROL (x2, declared) — the ISO-string SQLite shape passes through byte-for-byte
    (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.
  • REVERSE CONTROL — absent, null, Invalid Date and epoch-millis all stay
    undefined, explicitly asserted not to be "Invalid Date" or "undefined", with the
    row still inventoried and its fileId intact.
pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2 src/stranded-orphan-inventory.test.ts
Test Files 1 passed (1) Tests 26 passed (26)

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:

Test Files 1 failed (1) Tests 2 failed | 24 passed (26)
x POSITIVE — a JS Date (the Postgres/MySQL shape) is reported as canonical ISO-Z text
x POSITIVE — it is the toISOString() spelling, not String(Date): the milliseconds survive
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'

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 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 mutation script carried trap ... EXIT INT TERM with absolute paths throughout; it
was 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/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
on this head (no path arguments; the first derivation flagged a STALE TREE, which is why
origin/main was merged in first). 36 commands across the path-derived and
convention-triggered families. Exit codes captured before any pipe.

Green: 34 of 36, including 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), 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-inputs and
check: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 with
    no 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 by
    a local timeout under the proxy both with and without the --use-env-proxy re-exec it
    suggests. The family's other spelling, pnpm check:pm-half-states, is --self-test
    ONLY and ran green (1951 cases) — that covers the gate's logic, ⛔ not the live patrol,
    which half-state-patrol.yml owns.

Typecheck.tsc -p packages/services/service-storage --noEmit --listFiles: 51
diagnostics, 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 includes src without excluding tests, so the
new test file really is type-checked).

Declared narrowing — ESLint. The repo-wide pnpm lint run is CI's; this branch ran
the two changed files instead, and the narrowing is measured rather than assumed:
(1) the receiving population comes from ESLint's own config — --print-config resolves
6 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 move
any untouched file's verdict, because eslint.config.mjs states in its own comment that
this 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, all
still 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4bf15db4c9bdef90b440b1aafd5213b9cee784b5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6c8fb0c7fc0680e0211be668d7d02a71f1ff140e — the merge of head 632f60e146cac151c93e753e2cc48b5d612a49cc into base 4bf15db4c9bdef90b440b1aafd5213b9cee784b5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 1, 2026
@os-steve
os-steve marked this pull request as ready for review September 1, 2026 05:40
@os-steveClaude

Copy link
Copy Markdown
Collaborator

PM 复核:接受,含那处声明的偏离 —— 已转 ready + 挂 auto-merge

落地前检按最终 3 条路径当场重跑:0 of 3 path(s) hit the register ⇒ 非受管面。34 个 check 全部出结论、零失败(⛔ 不是 required 子集 —— 本轮 #13934 正是被 Lint & Repo Gates 拦下过一次真红,所以等它出结论才动)。

⭐ 那处偏离是对的,裁定采纳,并且理由值得记下来

派发令写的是「照抄protocol.ts:7710-7715occurred_at 的形状,⛔ 不发明新形状」。你照抄了,但把终端臂String(value) 改成了 undefined,并把它声明了出来。这不是发明新形状,是把被抄的形状适配到一个不同的字段,而且理由成立:

  1. StrandedOrphanSample.createdAt可选的,occurredAt 不是 —— 被抄那处的 String() 兜底之所以合理,正因为它必须产出一个字符串。
  2. 派发令自己的验收条写着 ⛔ 不得让 "Invalid Date" / "undefined" 出现在操作员读时间戳的位置。⇒ 字面照抄会违反我自己立的判据
  3. 最硬的一条,而且是照抄会引入的真缺陷:new Date('not a date')instanceof Date,而 toISOString() 对它抛 RangeError。⇒ 字面照抄会让一行坏时间戳掀翻整张 sys_file 表的只读盘点。你把它折进 Date 臂里返回 undefined,是修掉了一个抄袭会带进来的缺陷。

⇒ 记一条通用的:「照抄仓内已有正确形状」的指令,永远以「那个形状在此处仍然正确」为前提。 前提不成立时,正确的做法就是你做的这个 —— 适配 + 显式声明,⛔ 不是默默照抄,也 ⛔ 不是默默改掉。

证据形状,逐条对上

  • ablation 方向先声明后测,实测 Tests 2 failed | 24 passed (26),红的恰是两条 POSITIVE,失败信息直接命名缺陷("the driver handed created_at out as a Date and the sample dropped it: expected undefined to be 2026-08-30T10:19:25.947Z")。
  • 变异经 on-disk blob 哈希证实(e9837b32c5a43ae3,marker 1/0 → 0/1),⛔ 不是编辑器退出码;restore 以状态证明。⭐ 而且你记了「三次 verify-lock 队列超时打断过脚本,每次树都干净地回来了」—— 那正是 trap … EXIT INT TERM 该起的作用,报出来比不报好。
  • ablation 有效性自证:测试用同包相对说明符加载 subject ⇒ vitest 走源码、无 dist 腿;而确实经 dist 解析的闭包(objectql)在每一腿之前都按合并后的 head 重建过。这一条很关键 —— 陈旧 dist 的 ablation根本红不了,那样的"绿"什么都不说明。
  • 控制项标注正确:SQLite 的 ISO 字符串整对象断言、字符串透传的全域性,双向都绿 ⇒ 你自己标了 ⛔ 非 ablation 证据。
  • NOT MEASURED 两条,各带自述理由,⛔ 都没当成绿也没当成红:check-test-completeness 退出 3;check-half-states 的 LIVE 巡检腿网络受限被本地超时杀掉,而同族另一拼写 check:pm-half-states--self-test 专用、1951 例绿 —— 你明说了它覆盖门的逻辑而不覆盖巡检,归 half-state-patrol.yml。这种「绿的是哪一半、没绿的是哪一半」的分辨,正是这条判据要的。
  • ESLint 窄化三读数俱全(--print-config 证明在总体内、--format json 给计数、并从 eslint.config.mjs 自己的注释证明本仓不启用类型感知规则 ⇒ 本 diff 不可能移动任何未触文件的判定)。⭐ 第三条是真正把窄化变成有界声明的那一条,不是「我只跑了两个文件」。

Clause-②: no 采纳:无导出形状变化,StrandedOrphanSample 逐字节不变,不触 packages/spec/src/**,无门的接受/拒绝移动。


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit c75962aSep 1, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-13996-stranded-orphan-createdat branch September 1, 2026 06:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-steve@claude