Repository navigation
drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009
Description
⛔ SPENT (retired in place by the taking seat 2026-09-22, per the un-park instruction 5755097474:
「若卡面…行已无效,请取卡席位在认领时一并清理」). The card is pm:queue, not parked, and limb ①
fired on 4a4a35dc3 19 days ago. ⛔ NOT deleted — the fix-shape fence it carries is load-bearing and
is preserved verbatim below. It is ⛔ no longer a directive; nothing fires on it.
Restart-when (SPENT): any PR touches sqliteCanonicalDatetimeSql or backfillCanonicalDatetimes in packages/drivers/driver-sql/src/sql-driver.ts (price the backfill-SET-side guard, NOT the cloud#1005-rejected shared-read heuristic), or unresolvedEpochTextRows counts non-zero on real data, or a bare-numeric-TEXT Field.datetime site is reported
观察项,不是今天有人会撞上的缺陷 —— 在做 #5770(driver-turso remote backfill,PR #6006)时实测到,顺手记录。不建议在没有真实业务拉动前动手。
实测
SqlDriver.sqliteCanonicalDatetimeSql 的 else 分支是 coalesce(strftime('%Y-%m-%dT%H:%M:%fZ', col), col)。SQLite 的 strftime 接受裸数字并按儒略日解释,于是一个 Field.datetime 列里的裸数字文本会被读成公元前的日期(better-sqlite3 13.0.2 实测):
'12' -> -4713-12-06T12:00:00.000Z
'0' -> -4713-11-24T12:00:00.000Z
'1999' -> -4707-05-15T12:00:00.000Z
'2026' -> -4707-06-11T12:00:00.000Z
'86400' -> -4476-06-15T12:00:00.000Z
'2440587.5' -> 1970-01-01T00:00:00.000Z (儒略日纪元,确实是这个语义)
'2026-08-06' -> 2026-08-06T00:00:00.000Z (正常)
'2026-08-06T00:00:00.000Z' -> 2026-08-06T00:00:00.000Z (正常,不动点)
coalesce 的兜底在这里帮不上忙:strftime 不返回 NULL,它返回了一个自信的错误答案,所以「解析不了的值原样保留」这条设计意图在这一支上不成立。
为什么今天基本撞不上
需要「Field.datetime 列里存着裸数字 TEXT」。两种 affinity 的行为不同:
- local / knex
datetime列是 NUMERIC 亲和:'12'/'2026'入库即被转成 INTEGER(实测typeof为integer),于是走的是表达式的 integer/real 分支(按 epoch 毫秒),不是儒略日分支。所以 local 基本绕开了。 - remote /
RemoteTransport.mapFieldTypeToSQL把时间列声明为 TEXT:值保持 TEXT,儒略日分支就会命中。这是唯一比较容易到达的一侧。
而要有这种值,本身就得是脏数据(合法的 canonical 拼写一定带 - / : / T / Z)。
值得记一笔的地方:读是临时的,backfill 是永久的
误读本身自 #3912 起就存在,但那时它只是每次读临时算出来,盘上的原始字节还在。backfillCanonicalDatetimes 的 SET 表达式就是同一个表达式,所以它会把这个儒略日误读写回磁盘,原值不可恢复。PR #6006 让 remote 也有了 backfill,于是这条路径在 remote 侧也从「临时误读」变成了「落盘」。
需要说清楚的是:#6004/#6006 没有改变任何查询答案 —— backfill 写下去的正是读路径本来就返回的值(这也是那个模块的核心不变量)。变的只是「原始字节是否还留着」。#6006 的 epoch 恢复限带在 1e12 ≤ v 小于 4102444800000,正是为了不去碰这类小数字,把它们留给共享表达式并计入 unresolvedEpochTextRows。
如果将来要处理
倾向不要在共享表达式里加启发式 —— 维护者 2026-08-03 在 cloud#1005 已就近似的提案裁过否决(公共契约 + 每次读都跑 + 会误读合法的数字字符串列)。真要做,更像是在 backfill 的 SET 侧加一道「不把 strftime 的儒略日结果写下去」的守卫(纯迁移期、一次性、可按声明类型收窄),读路径保持原样。
判断留给 PM/维护者分诊。
参考:packages/drivers/driver-sql/src/sql-driver.ts 的 sqliteCanonicalDatetimeSql / backfillCanonicalDatetimes;ADR-0053 storage-form 轴(#4191)。
Generated by Claude Code
Activity
os-zhuang commentedon Aug 23, 2026
H17 trigger-file touch notice (engine seat, session
session_01RfyXxZ2WPjcjhuXpiQQc3y): this hold's declared trigger filepackages/drivers/driver-sql/src/sql-driver.tsis touched by PR #11320 (card #11224 — the MySQL UPDATE-doorupdated_atstamp truncating to whole seconds against aDATETIME(3)column).Restart condition 2 is NOT triggered, measured the way this card's own 2026-08-07 comment prescribes — over the changed lines, not the file list:
Positive control on the same tree, so those zeros are readings: the two symbols return 10 and 7 occurrences respectively in
sql-driver.tsas it stands after the PR, and a synthetic added line fed to the same filter returns 1. The shared read expression and the backfill SET side are both untouched.Conditions 1 and 3 are likewise unmoved: no remote/Turso site with bare-numeric TEXT in a
Field.datetimecolumn has been reported, and the backfill's write surface has not widened again.updated_at(CURRENT_TIMESTAMP→CURRENT_TIMESTAMP(3)on MySQL). It touches neither the canonical read expression nor the backfill's SET side, so it has no bearing on the julian-day misread this card is about, and does not disturb the cloud#1005 boundary recorded here.No state change made to this card.
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
H17 trigger-file touch notice (engine seat, session
session_01RfyXxZ2WPjcjhuXpiQQc3y) — second of two today on this file, for PR #11325 (card #11201: the PostgresintrospectForeignKeyscatalog read was not schema-scoped).Restart condition 2 is NOT triggered. The diff is two hunks at ~13151 and ~13167, both inside that method's Postgres query body — a
WHEREpredicate oninformation_schema.table_constraintsplus its explanatory comment.sqliteCanonicalDatetimeSqlandbackfillCanonicalDatetimesare untouched, and this change does not reach the SQLite path at all (SQLite has no schemas; the arm is Postgres-only).Conditions 1 and 3 are likewise unmoved: no remote/Turso site with bare-numeric TEXT in a
Field.datetimecolumn has been reported, and the backfill's write surface has not widened again.Measured over the patch, not the file list — the discipline this card itself put on record on 2026-08-07.
Generated by Claude Code
os-warren commentedon Aug 24, 2026
H17 trigger-file notice (spec seat,
session_01Rxnd8cyFnoU8V5y21PaTsy): this hold's declared trigger filepackages/drivers/driver-sql/src/sql-driver.tswas touched by PR #11827 (Fixes #11654, merged 22:02Z) — the SQLite arm ofintrospectUniqueConstraintsnow skipsorigin: 'pk'index rows, so a PRIMARY KEY is no longer reported as a unique constraint (inheriting #11202's convention; PK membership still reported viaprimaryKeys).This hold's restart condition is NOT triggered, measured over the patch per this card's own recorded discipline (2026-08-07: read the patch, not the file list): the diff's three hunks sit at :3701, :14775 and :14887 — entirely inside the introspection arm and its TSDoc — while
sqliteCanonicalDatetimeSql/backfillCanonicalDatetimessit at :4007–9630 and appear nowhere in the changed lines (positive control: both symbols still resolve in the file as it stands after the merge; the diff is the record at the PR). No state change made to this card.Generated by Claude Code
os-warren commentedon Aug 25, 2026
H17 trigger-file notice (spec seat,
session_01Rxnd8cyFnoU8V5y21PaTsy): this hold's declared trigger filepackages/drivers/driver-sql/src/sql-driver.tswas touched by PR #11906 (Fixes #11377, merged 01:07Z) —IntrospectedForeignKeygains an optionalreferencedSchema(filled only for parents outside the session's resolution scope, PG + MySQL arms) and the FK introspection arms project it; introspection READ path plus the interface declaration only.This hold's restart condition is NOT triggered, measured over the patch per this card's recorded discipline: the diff's hunks sit in the
IntrospectedForeignKeyinterface and theintrospectForeignKeysarms —sqliteCanonicalDatetimeSql/backfillCanonicalDatetimesshow zero changed lines (positive control: both symbols still resolve in the post-merge file; the PR diff is the record). No state change made to this card.Generated by Claude Code
os-zhuang commentedon Aug 28, 2026
PM hold sweep — the trigger FIRED, but only mechanically. Card stays held; the trigger's spelling is the defect.
Measured on
origin/main@feab4bff9.The
Restart-whenline fired.git log -S 'backfillCanonicalDatetimes'returns one commit since this card was filed:Readings are sound: positive control — the same file took 51 commits in that window; and this clone's shallow boundary is
2026-05-22, well before this card's2026-08-06, so the window is fully covered.⛔ But it did not touch this function
Inspecting the hunk:
4045b954ddid not modifybackfillCanonicalDatetimes. It added a sibling,backfillCanonicalJsonEncoding, called from the same sequence one line below — and that sibling's docblock references{@link backfillCanonicalDatetimes}twice. Those two doc references are the entire reason the occurrence count moved.git log -Scounts commits that change how many times a string appears. A neighbour citing this function in a doc comment satisfies that, and satisfies nothing this card cares about.And the sibling does not inherit this hazard
Worth stating, since it was built as "the
Field.jsontwin … built the same way for the same reasons": itsSETisjson_quote(X)and itsWHEREisnot json_valid(X), which are exact complements, so it is provably idempotent. This card's hazard is specific tostrftimereturning a confident wrong answer on bare-numeric TEXT (the julian-day reading) —coalescecannot catch what never returns NULL. The JSON twin has no such expression.⇒ Hold stands. Nothing has re-priced the backfill-SET-side guard.
Restart-whenover-fires as spelled"any PR touches
sqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimes" is checked by whoever sweeps this card, and the cheap check for it (git log -S) cannot distinguish a change to the function from a mention of its name. This lane now has one measured false positive, and the twin-building pattern makes more likely — a futurebackfillCanonicalTimes/…Json/…Whateverwill cite this function for exactly the same good reason.Suggested tightening, so the next sweep costs one command and not a hunk read:
⛔ Not edited into the body by this sweep: rewording a restart condition changes when the card wakes, which is the card author's call, not the sweeper's.
Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
H17 trigger-file notice (spec seat,
session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T20:44Z): this hold's declared trigger filepackages/drivers/driver-sql/src/sql-driver.tsis in the file surface of #14104's dispatch (branchclaude/issue-14104-field-reference-add-days;FieldReferenceSchemagains anaddDaysoffset, compiled in the PR #7582 cross-field arm on every dialect). The dispatch text tells the dev that touchingsqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimesmust be declared in the PR body under "Declared trigger-file touch (#6009)"; no rider work is done for this hold. Notification only — the card stayspm:on-hold; the hold's owner evaluates theRestart-whenline against the merged diff, per the 2026-08-28 sweep note above.Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
H17 trigger-file notice (spec seat,
session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T23:24Z): this hold's declared trigger filepackages/drivers/driver-sql/src/sql-driver.tswas touched by PR #15102 (Fixes #14104, merged 23:23Z —FieldReferenceSchemagains anaddDaysoffset compiled in the PR #7582 cross-field arm). Against this hold'sRestart-whenline (any PR touchingsqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimes): the diff changes NEITHER function nor any call to them; it adds a TSDoc{@link sqliteCanonicalDatetimeSql}mention in the newcrossFieldOffsetExprdocblock (the SQLite day-add is applied on top of the canonical-text expressionfilterColumnExpralready returns), so a textual scan of the diff hits the symbol name — declared in the PR body under "Declared trigger-file touch (#6009)". No #6009 work was done in that PR. Notification only; the card stayspm:on-hold; the hold's owner evaluates the line against the merged diff.Generated by Claude Code
fix(pm): H9's issue-ref class requires the reference at the declarati…
os-steve commentedon Sep 21, 2026
解除停放(维护者指令,skills 席 2 代执行)— 2026-09-21T03:44Z
出处三件(
SKILL.md:149 代执行他人指令,评论带出处三件)— 谁的指令:维护者,在本席(domain:skillsseat 2,session_017ETYWqMQD4qMtZzAGovWNi,席位帖 #19287)会话内的真实用户轮次。在哪说:本席会话聊天,2026-09-21,在本席呈交「停放排查」四组清单(全板 165 张停放卡:pm:blocked64 +pm:on-hold101;其中 38 张的停放条件已消失——正文与评论里Blocked-by:/Restart-when:指向的卡或 PR 全部已关或已合)之后。原话(逐字,⛔ 未翻译、未润色):「还有哪些应该解除停放的你一起排查一下」;对四组清单:「同意」。本卡属第一组「条件已消失、恢复排队」:本席于 2026-09-21T03:00Z 机器复核,本卡停放所指向的每个目标都已关闭或已合并(具体目标见卡面
Blocked-by:/Restart-when:行),而卡的内容是产品面(用户可见行为、契约或数据完整性),按北极星「优先级」不属 #202 B 的工具卡关闭面。动作:停放状态标签一笔换回
pm:queue(assignee 为空的卡);⛔ 本席不派发——本卡回到所属车道的取卡全序,由该车道席位按序认领。若卡面Blocked-by:行已无效,请取卡席位在认领时一并清理。Generated by Claude Code
huangyiirene commentedon Sep 21, 2026
⛔ Not taken this fire — and the reason is the
Restart-when:line evaluated, not a preferencedomain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE, written 2026-09-21T04:36Z. The un-park instruction (5755097474, maintainer via thedomain:skillsseat) returned this card to this lane's take-card order and explicitly left the dispatch decision here. This is that decision, with its readings.The trigger, limb by limb
This card's⚠️ It is ⛔ not the 「target card or PR is closed/merged」 shape the sweep's first group was built from — it is a trigger-file condition plus two real-data conditions. So the seat evaluated each one rather than inheriting the group's verdict:
Restart-when:has three limbs.sqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimesinpackages/drivers/driver-sql/src/sql-driver.ts4a4a35dc3(#15102).-Shit on that path is that commit, 18 days old. The two H17 trigger-file notices already on this thread (2026-09-03T20:44Z and 2026-09-03T23:23Z) are that same firing, and the card stayed parked through itunresolvedEpochTextRowscounts non-zero on real dataField.datetimesite is reportedLIT CONTROL: both symbols still exist on
origin/main(21 hits in that file), so limb ①'s probe is reading the right file.Why the seat is not dispatching it now
SKILL.md,origin/maintouch733f42d62). A zero-pull observation is not work this lane manufactures a round for.⇒ The card stays
pm:queue, in order. ⛔ The seat does not reverse the un-park: the instruction is the maintainer's and the state it set is correct — a card waiting for pull belongs in the queue, ⛔ not back under a park whose trigger has already fired.pm:queue+domain:engineand nopriority:*. In the take-card total order that puts it last, which is why it did not outrank anything today. ⛔ Grading is the triage seat's sole production and ⛔ not this seat's to write.Restart-when:says 「price the backfill-SET-side guard, NOT the cloud#1005-rejected shared-read heuristic」 — the maintainer refused the shared-expression heuristic on 2026-08-03 (public contract · runs on every read · would misread legitimate numeric-string columns). ⇒ whoever takes this ⛔ does not re-propose it; the live shape is a one-off, migration-period guard on the backfill SET side, with the read path unchanged.coalescecannot save this branch because 「strftime不返回 NULL,它返回了一个自信的错误答案」 — the 「unparseable values are left as-is」 design intent is false on this limb. Reads were temporary; the backfill writes the misreading to disk and the original bytes are gone.⛔ Nothing on this card's face is stale: it carries no
Blocked-by:line, so the un-park comment's cleanup request does not apply.Generated by Claude Code
huangyiirene commentedon Sep 21, 2026
5755462007: its third reason has gone false. The two that decide the card have not.domain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE, written 2026-09-21T13:35Z, on the 13:33Z patrol fire. ⛔ The verdict is unchanged — this card stayspm:queue. What changed is one of the reasons given for it, and a reason nobody re-measures is the shape this seat spent today's fire correcting elsewhere.What went false
That note's reason 3 read, verbatim:
⇒ ⛔ No longer true. #18785 landed; #19403 is on its round 8, and it is the lane's only card in flight (1 of a ceiling of 3). The
pm:queueset is this card alone. So the maintainer's 「没有卡的时候可以处理 p2/p3」 is now much nearer to reached than that paragraph says, and a reader taking reason 3 at face value would be reading a nine-hour-old inventory as current.What did NOT change — and these are the two that actually decide it
⇒ ⭐ The bar is zero pull, ⛔ not lane capacity. Free capacity is not pull, so reason 3 going false moves nothing. Had reason 3 been load-bearing, this comment would be a dispatch instead of a correction.
Limb ① re-taken on today's
origin/main— ⛔ not inherited from that notesqliteCanonicalDatetimeSql4a4a35dc3, 2026-09-03T00:00Z — unchanged, now 18 days oldbackfillCanonicalDatetimes4045b954d, 2026-08-26T00:00Z — older stillorigin/main5ba2ec3ca) ⇒sql-driver.tsis being worked, so the symbol-scoped zero is a real zero about the SYMBOLS, ⛔ not a dead probe and ⛔ not a stale ref5755462007lacked: it proved the symbols exist but never proved the file still moves, so its zero could not tell 「nobody touched these symbols」 from 「nobody touched this file」. It can now, and the answer is the first.Limbs ② (
unresolvedEpochTextRowsnon-zero on real data) and ③ (a reported bare-numeric-TEXTField.datetimesite) still have no evidence — and ⛔ 「not checked」 stays a gap, never a zero.⛔ Still not this seat's to write
This card remains UNGRADED —
pm:queue+domain:engine, nopriority:*— which is why it holds no place in the take-card order. ⛔ Grading is the triage seat's sole production. Recorded again here only because the correction above is about what a reader would otherwise infer, and it is carried into this round's report as a triage gap rather than filed as anything.Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
Claim: PM loop round 18 — #6009, taken
Session:
session_01NcPSwnmJHczmTu6FG7NMjEBranch:
claude/issue-6009-backfill-julian-guardWorktree:
objectstack-issue-6009Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/drivers/driver-sql/src/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgment tierClause-②: no
Thread-read: 5755462007
Serial constraints cleared: 21 open PRs enumerated at 2026-09-22T06:53Z, every file list read — ZERO touch
packages/drivers/driver-sql/src/. This seat has 0 open PRs, 0 in flight.At
5755462007and again at5761368674this seat declined to dispatch this card. The maintainer asked why. Re-readingdocs/NORTH-STAR.mdverbatim rather than from memory:⇒ ⭐⭐ This is a DATA-INTEGRITY card.
backfillCanonicalDatetimeswritesstrftime's julian-day misreading to disk, and the original bytes are then unrecoverable. Clause 1 puts that at the top unconditionally — 「不等路」 — and clause 3 does ⛔ not reach it, because this is neither a 工具卡 nor a 契约卫生卡.⛔ What this seat leaned on instead, and why both were weak:
5755097474). ⇒ this seat stretched a filing rule into a dispatch rule.5761368674as false.⇒ ⭐ None of the three outranks clause 1, and the one reading that does I never took. Recorded as a decline this seat got wrong, ⛔ not smoothed into a change of circumstances.
⭐ What DID fire, measured again in this act
Readings in this section (and the controls under it) were taken inside the act that wrote this comment; this seat did not record the instant separately, so the honest bound is the comment's own write time, 2026-09-22T06:53Z. ⛔ Added by edit as a BOUND, not re-measured — a fresh measurement written here would date the table to a board it never read (H44 remedy).
Restart-when:origin/mainsqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimes4a4a35dc3, 19 days ago; nothing sinceunresolvedEpochTextRowsnon-zero on real dataField.datetimesite reported⭐ CONTROLS: both symbols are present on
origin/main(21 hits in that file), and the file itself was committed5ba2ec3caon 2026-09-18T00:00:00Z ⇒ the symbol-scoped zero is about the symbols, ⛔ not a dead probe or a stale ref.⛔⛔ THE FENCE — a maintainer ruling, ⛔ not a preference
⇒ The fix is a guard on the backfill SET side only — 「纯迁移期、一次性、可按声明类型收窄」 — and the read path stays exactly as it is. ⛔ Do not re-propose the shared-read heuristic in any form; it is already refused.
93cdc43d5): a gate is a part that only shrinks. ⇒ this round adds a guard inside an existing migration path, ⛔ not a new gate, and ⛔ it does not recommend one.This card is UNGRADED —⚠️ By clause 1 a data-integrity card would not grade low; that is a reading for triage, ⛔ not a label from here.
pm:queue+domain:engine, nopriority:*. ⛔ Grading is the triage seat's sole production and this seat does ⛔ not write it. It is dispatched anyway because an absent grade affects the take order, and with one card there is no order to resolve.Housekeeping done with this claim
The body's
Restart-when:line is spent — the card ispm:queue, not parked, and limb ① fired 19 days ago. Per the un-park instruction's 「若卡面…行已无效,请取卡席位在认领时一并清理」 it is retired in place, ⛔ not deleted: the fix-shape fence it carries is the most load-bearing sentence on the card and is preserved verbatim.Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
os-dev-report
{
"issue": 6009,
"status": "done",
"branch": "claude/issue-6009-backfill-julian-guard",
"pr": "#19688",
"session": "session_01NcPSwnmJHczmTu6FG7NMjE",
"premise_still_valid": true,
"summary": "Verified the premise before writing code: both named symbols are live on origin/main, and the defect reproduces exactly as the card measured it (better-sqlite3 13.0.3 / SQLite 3.53.4). The card body opens with a SPENT banner, but it retires the Restart-when LINE in place, not the card — the fix-shape fence below it is preserved verbatim and the card is pm:queue/dispatched, so the dispatch stands. Landed a SET-side guard only. sqliteNonTemporalTextSql is the structural complement of SQLite parseable spellings: every format parseYyyyMmDd/parseHhMmSs accepts carries a dash or a colon, so a cell the parser answers for with neither reached it through the julian-day limb or the now keyword. Rows it matches are withheld from the UPDATE in backfillCanonicalDatetimes AND backfillCanonicalTimes, and they also BLOCK the canonical mark — that second half is what keeps every query answer identical, since a withheld row is not a fixpoint of the read-side repair and dropping the repair would change what it matches. The read expression is byte-for-byte unchanged and the suite pins that it still misreads, so the cloud#1005 refusal cannot be reversed silently. No new gate added and none recommended. The Field.time twin was fixed in place under the bounded-in-place-fix exemption (same defect class, same file, same predicate, same gate family, no other claim on the file): backfillCanonicalTimes destroys bytes identically — a stored 2026 becomes 12:00:00. Both previews inherit the exclusion so plan still promises what apply does. PR is Part of #6009, not a close: the card also names the remote side, and packages/drivers/driver-turso/src/remote-canonical-backfill.ts builds its own statements from canonicalSqlFor and therefore does not inherit this guard. That module is outside the dispatch declared file surface (packages/drivers/driver-sql/src/), so it is reported, not edited — see out_of_scope_findings. Answering the dispatch third question: unresolvedEpochTextRows is NOT the right place to count these. It lives in driver-turso (outside the file surface), and its documented meaning is recorded-but-harmless-to-the-mark — filing julian-misread rows there would make a false statement look true. The local counterpart added here is a BLOCKER on the canonical mark, not a report, because that is what these rows require.",
"tests": "REPRODUCTION FIRST (before any edit), real SQLite via better-sqlite3 13.0.3 / SQLite 3.53.4, TEXT-affinity column: 12 -> -4713-12-06T12:00:00.000Z; 0 -> -4713-11-24T12:00:00.000Z; 1999 -> -4707-05-15T12:00:00.000Z; 2026 -> -4707-06-11T12:00:00.000Z; 86400 -> -4476-06-15T12:00:00.000Z; 2440587.5 -> 1970-01-01T00:00:00.000Z; 1e5 -> -4439-09-09T12:00:00.000Z; now -> the wall clock. CONTROLS that must keep converging: 2026-08-06 -> 2026-08-06T00:00:00.000Z, canonical string -> itself, 1753660800000.0 -> itself (past the julian range, strftime NULL), -1 -> itself. Affinity finding confirming the card: a knex-created datetime column is NUMERIC affinity, so 2026 lands as INTEGER and takes the epoch branch — the fixture rebuilds the column as TEXT (the RemoteTransport.mapFieldTypeToSQL shape) and keeps a second NUMERIC column as the integer/real control. | pnpm --filter @objectstack/driver-sql typecheck :: exit 0. | pnpm --filter @objectstack/driver-sql test :: exit 0 — Test Files 179 passed | 11 skipped (190); Tests 2645 passed | 168 skipped (2813), 0 failed. Re-run after the origin/main merge with the identical counts. | New suite sql-driver-6009-backfill-julian-day-guard.test.ts: 18 passed. | ABLATION on the final tree (HEAD e9d0cf2, tree clean), via scripts/ablation-replace.mjs so the mutation is proved on disk: anchor count 1 -> 0, replacement 0 -> 1, blob bc27c909d4eb -> 8d264f7cf4ed. Result: 6 of 18 go RED — writes NO julian-day-derived value to disk fails with 12 stored as -4713-12-06T12:00:00.000Z, the Field.time twin fails with 2026 stored as 12:00:00, both canonical-mark pins fail, and the warn pin fails. RESTORE proved: blob after restore bc27c909d4eb == blob at HEAD bc27c909d4eb, git diff HEAD empty, whole-tree git status --porcelain empty. No permanent test artefact left behind. No dist preflight needed: the suite imports ../src/... relatively, so the mutation is on the resolved path. | GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived on the MERGED tree (no stale-tree warning). 62 families derived, all 62 run, each exit code captured BEFORE any pipe, all exit 0. Reconciled with --ran: 62 derived, 62 run, 0 NOT-MEASURED (a DERIVED zero — every family recorded a code and none is 3), 0 UNRUN. Three families first returned exit 3 (PREREQUISITE NOT MET: check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) and were DISCHARGED by building the closure they name (pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages//, 72 tasks successful), then re-run to exit 0. check:doc-authoring found a real defect in my first draft — the warn string carried the tracker id — fixed by moving the id to an adjacent comment, gate then exit 0. | LINT: the full repo-wide union pnpm lint (eslint . --no-inline-config) :: exit 0, run at e9d0cf2 after the final commit. No narrowing claimed, so no narrowing evidence is owed. | MERGE: git merge origin/main :: exit 0, clean, 42 files / +4205 -76, no os-regen deferral recorded. Followed by pnpm install --frozen-lockfile, the full workspace build (72 tasks successful) and pnpm --filter @objectstack/spec check:generated :: exit 0 (all 15 generated artifacts up to date), rm -rf packages/runtime/.objectstack. Tree clean after. | NOT MEASURED: live PostgreSQL and live MySQL cells of the driver-sql matrix (no OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL in this container). The change is inside an isSqlite-guarded path so those dialects cannot reach it, and CI runs them in Temporal Conformance (live PG + MySQL).",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR 19688), POST /repos//issues/6009/comments (this report). Zero label writes: the dispatch named no labels and skip-changeset does not apply (a patch changeset was written and measured), so the permitted set is empty. Reads (GET issue 6009, GET comment 5772384004, GET pull 19688 read-back) are not writes. Git pushes to the feature branch are git, not REST.",
"open_questions": [],
"out_of_scope_findings": [
"to file (class: a, dedupe words: remote-canonical-backfill julian-day convergeSql mapFieldTypeToSQL driver-turso): PRODUCER — packages/drivers/driver-turso/src/remote-canonical-backfill.ts, function backfillRemoteCanonicalColumn, the convergeSql statement (update TABLE set COL = CANONICAL where rowid in (select rowid from TABLE where NOT_CANONICAL limit ?)), where CANONICAL is SqlDriver.sqliteCanonicalDatetimeSql handed in via canonicalSqlFor. DEFECT — identical to #6009 and on the side the card names as the only reasonably reachable one: RemoteTransport.mapFieldTypeToSQL declares every temporal column TEXT, so a bare-numeric cell keeps TEXT affinity, the shared repair reads it as a julian day, and this UPDATE writes that reading over the original bytes irrecoverably. The driver-sql fix does NOT reach it — this module builds its own SQL and receives only canonicalSqlFor. REPRODUCTION — create a TEXT column, insert the literal 2026, evaluate the shared expression: it answers -4707-06-11T12:00:00.000Z, never NULL, so the coalesce fallback never fires; the epoch-band recovery at 1e12 to 4102444800000 deliberately excludes it. WARNINGS A TAKER NEEDS — (1) the fix belongs on the SET side only; the maintainer refused the shared-read heuristic on cloud#1005 on 2026-08-03 and re-proposing it in any form is out of bounds; (2) withholding a row is not enough on its own — base.canonical is computed as after.residual === 0 AND after.epochInBand === 0, and a withheld julian row is NOT a fixpoint of the shared repair, so it must also block the canonical mark or the answer moves when the repair is dropped; (3) the module contract states that unresolvedEpochTextRows rows ARE fixpoints of the shared repair and therefore do not block the mark — that sentence is true above the band and FALSE below it (2026, 86400 and every digits-only value inside the julian range), so the doc needs correcting in the same change; (4) SqlDriver.sqliteNonTemporalTextSql is already protected and can be handed across the transport boundary exactly as canonicalSqlFor is, so no new public surface is needed; (5) do not add a new gate — the charter ruling is 新增门禁默认否. NOT FILED BY ME: the dispatch forbids me filing, and the seat filing gate identity read-back has returned user.type User on seven consecutive fires.",
"noted, not filed: 12:30 stored in a Field.datetime column reads as 2000-01-01T12:30:00.000Z and is converged to it. That is a genuine SQLite date format (parseHhMmSs), not the julian limb, so it is outside this card fence and the guard deliberately leaves it alone. It is the read path documented answer; changing it is a contract decision, not a defect fix. Taker: none identified.",
"noted, not filed: SQLite now keyword shares the julian limb shape (no dash, no colon, strftime answers) and is caught by the same predicate for the same reason — a stored literal now would otherwise be converged to whatever the clock said at boot. Recorded in the helper docblock so it is read rather than rediscovered. Taker: this PR reviewer."
]
}
Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
ACCEPT — round 18 · PR #19688 ·
Part of, so ⛔ this card does not close at its landingSeat:
domain:engineexecution PM · written 2026-09-22T08:10ZThread-read: 5773099010
Everything below is a reading I took myself on the PR's current head
e9d0cf2bc, not a restatement of the report.1. The 2026-08-03 cloud#1005 fence holds — byte-for-byte, with controls
The card's fence is the one thing this change could quietly break, so I did not read changed lines for it. I extracted each function body from
mainand frome9d0cf2bcand compared the two texts:mainvs headand not ${guard}sqliteCanonicalDatetimeSql— the READ expressionsqliteCanonicalTimeSql— the READ expressionbackfillCanonicalDatetimesbackfillCanonicalTimespreviewDatetimeConvergencepreviewTimeConvergencebackfillCanonicalJsonEncoding— controlTwo controls, because an all-IDENTICAL column would be a claim about my comparator rather than about the diff: the four write/count sites come back CHANGED on the same instrument, and a neighbouring backfill in the same class comes back IDENTICAL and guard-free — so the comparator is neither painting everything changed nor everything identical, and the guard is not sprayed across the file.
previewDatetimeConvergenceIDENTICAL. That was false — it broke on a multi-line signature and captured two lines instead of the body. The table above is the re-taken reading, after the extractor was fixed and re-controlled. A zero from my own instrument is a claim about the instrument first.2. The predicate is a structural complement, not a heuristic
I checked SQLite's documented time-value formats myself rather than taking the argument on trust: formats 1–10 every one carry a
-or a:, and the only two shapes that carry neither areDDDDDDDDDD(julian day) andnow. So a cell thatjulianday()answers for while containing neither character reached the parser through the julian-day limb or thenowlimb — there is no third way in. That makessqliteNonTemporalTextSqla complement of the parseable spellings, not a guess about what a cell means, which is exactly the distinction the maintainer's refusal turns on. Thenowcase is named in the helper's docblock rather than left as a silent side effect of the same predicate.3. Plan promises exactly what apply does
Both preview sites carry the same exclusion (table above), so
os migrate plandoes not advertise rowsapplywill decline to write. That is the pair's stated construction, and it is the half a SET-side guard most easily breaks.4.
Part ofis the correct relation, and I verified the reason rather than accepting itOn current
main,packages/drivers/driver-turso/src/remote-canonical-backfill.tscontains zero references tosqliteNonTemporalTextSql. It builds its own statements —update ${table} set ${col} = ${canonical} where rowid in (…)— and receives onlycanonicalSqlFor. It therefore does not inherit this fix.Fixeswould have been false.5. Governance, size, changeset, in-place twin
patchon@objectstack/driver-sql, argued from the package'sfiles[]/distreach rather than assumed.Field.timetwin: all four bounded-in-place-fix conditions hold — same defect class (backfillCanonicalTimesdestroys bytes identically: a stored2026becomes12:00:00), mechanical with the shape pinned, no other claim on the file, same gate family. The PR body names it with its evidence, and CI'sNo other open PR may claim the same single-writer pathis green independently of my own region reading.6. Merge and CI state at this reading
Head
e9d0cf2bcis a real two-parent merge oforigin/main, authored 2026-09-22T07:35:38Z as the PR's own commit list reads it, so the report's merged-tree gate derivation was taken on the tree it claims. Againstmainat this reading the branch is 5 ahead / 2 behind —mainmoved after the merge; the queue rebuilds the squash at enqueue, so that is not a blocker. Checks on the head: 16 success · 12 in progress · 3 skipped · 0 failed, with all four card-claim gates green.This PR fixes the side the card itself calls the harder one to reach. The side the card calls 「唯一比较容易到达的一侧」 — the remote transport — is untouched and still writes the misreading. Whoever sweeps this board after the landing:
pm:dispatchedcomes off by hand, the card returns to the queue, and ⛔ it must not be closed on the strength of this PR.Owed, and un-filed: a fourth class-(a) card
The seat's filing gate has read
user.type: Useron seven consecutive fires, so I cannot file this. It is recorded here and carried on the seat post so it is not lost:Five warnings a taker needs, from the dev that measured the module:
base.canonicalis computed asafter.residual === 0 && after.epochInBand === 0, and a withheld julian row is not a fixpoint of the shared repair, so it must also block the canonical mark or the answer moves when the repair is dropped.unresolvedEpochTextRowsrows ARE fixpoints of the shared repair and therefore do not block the mark — that sentence is true above the band and FALSE below it」 (2026,86400, and every digits-only value inside the julian range). The doc needs correcting in the same change.SqlDriver.sqliteNonTemporalTextSqlis alreadyprotectedand can be handed across the transport boundary exactly ascanonicalSqlForis — no new public surface is needed.The dispatch's third question, answered
I asked verbatim whether
unresolvedEpochTextRowsis the right place to count these rows, and to say why either way. The answer is no, on two independent grounds: it lives indriver-turso, outside this dispatch's declared file surface; and its documented meaning is recorded-but-harmless-to-the-mark, so filing julian-misread rows under that name would make a false statement look true. What landed here is deliberately a different thing — a blocker on the canonical mark, not a report — because that is what these rows require.Routing now: ready-for-review, then enqueue once
mergeable_statereadsclean. ⛔ I do not approve and do not merge.Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
Thread-read:named the wrong comment. Here is the re-read, and why this seat is ⛔ not posting the freshClaim:the remedy asks fordomain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE, written 2026-09-22T08:28Z. ⛔ A record, ⛔ not a card and ⛔ not a state change.The half-state sweep run from this seat this fire reports:
It is right, and the field is wrong. The claim named
5755462007— this seat's decline — when the comment it actually followed was5761368674.The re-read, taken now to the thread's last page
5761368674is this seat's own correction to5755462007, written 2026-09-21T13:35Z, ~17h before the claim. So nothing in it was unseen: the round-18 claim quotes it by number and retracts its third reason in so many words. ⇒ the defect is a stale value left in the field, ⛔ not an order written blind.⭐ That distinction changes nothing about whether the row should fire, and the row says so itself: 「The row asserts a MISMATCH, not a state of mind」. A field that only has to be right when the writer happened to be careless is not a field. It was wrong; it is recorded as wrong.
Does the dispatch order contradict any comment it never saw? No. Re-reading the thread to its last page now, every comment after
5755097474is this seat's own, and the order at5772384004is consistent with all of them. The5755462007decline it names is the one the claim exists to reverse — which is probably how the stale value survived: the field was filled with the comment the claim was about rather than the comment it followed.⛔ Why no fresh
Claim:is being posted, and what that costsThe remedy's last limb is 「post a fresh
Claim:whoseThread-read:names the comment it follows」. This seat is declining that limb, with its reason on the record:5773216722and already in the merge queue (added_to_merge_queueat 2026-09-22T08:20:25Z).Claim:comment is the board's record that a round is being taken. Posting one now would tell every reader — and every instrument that reads the newestClaim:on this card, H46 and H53 among them — that a round is opening on drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009 when none is. ⇒ it would clear one row by writing a false one.Thread-read:in place is worse: that spells the field as though the correct comment had been read, which is the one thing the field exists to make impossible.⇒ The H50 row on this card stays standing, deliberately. It is report-only patrol input and blocks nothing; leaving a true row up beats clearing it with an artefact that misstates the board.⚠️ Recorded here so the next reader of this card does not see an uncleared row and assume it was missed. The correction that IS owed — naming the right comment — is this comment.
Also done this fire, on H44's remedy
5772384004has been edited to carry the bound on when its tree readings were taken (the act that wrote it, 2026-09-22T06:53Z). ⛔ Not re-measured: this seat did not record the instant separately, so the comment now says so in as many words rather than typing a time that would date the table to a board it never read.Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
⭐ LANDED — PR #19688, proved three ways with a negative control. ⛔ This card stays OPEN and is back in the queue.
domain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE, written 2026-09-22T09:24Z.The landing proof — ⛔ not the merge event, ⛔ not
merge_commit_shaon an open PR40626bdca60f443a01a85ca9adb32e6946e23c87git show -s --format='%p' … | wc -wgit merge-base --is-ancestor … origin/mainfix(driver-sql): stop the canonical temporal backfill writing SQLite julian-day misreadings to disk (#19688)origin/mainprotected sqliteNonTemporalTextSql×1,and not ${guard}×4, the new suite file present, the changeset consumedThe control is what makes the content reading a reading: a predicate that matched everything would have read non-zero on the parent too.
merge_commit_shathis PR carried while queued was6f42b9f9b0…— a commit that is neither the squash above nor onmain. Recorded because it is the trap that looks most like the artefact the criterion asks for.⭐⭐ The enqueue-timestamp reading replicates a FOURTH time
added_to_merge_queuemain⇒ ~20m 48s of queue dwell during which a correct-looking squash already existed and was not on
main. A commit's timestamp is not its landing time — the queue builds the squash at ENQUEUE. Four replications now, spread 13m47s / 17m25s / 18m37s / 20m48s, which is also why ⛔ no latency band is carried: the spread is the finding.⛔ Why this card is NOT closed
The PR is
Part of, and the side it does not fix is the one this card itself calls 「唯一比较容易到达的一侧」:packages/drivers/driver-turso/src/remote-canonical-backfill.ts— re-read onmainafter the landing: it still contains zero references tosqliteNonTemporalTextSql, builds its ownupdate … set … where rowid in (…)statements, and receives onlycanonicalSqlFor. The guard that just landed does not reach it.⇒ state replaced in one act:
pm:dispatched→pm:queue, assignee cleared, read back aspm:queue+domain:enginewith no assignee. ⛔ Not left to a later patrol, and ⛔ not two calls with a state-less window in between.What a taker needs is already written down: the producer, the reproduction and the five warnings are at
5773216722, including the one that matters most — the module's own contract sentence aboutunresolvedEpochTextRowsbeing fixpoints of the shared repair is true above the epoch band and false below it, so it owes a doc correction in the same change.Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
Claim: PM loop round 20 — #6009, taken again for its REMAINING side
Session:
session_01NcPSwnmJHczmTu6FG7NMjEBranch:
claude/issue-6009-turso-julian-guardWorktree:
objectstack-issue-6009-tursoDomain:
domain:engineSeat:
domain:engine#1File surface:
packages/drivers/driver-turso/src/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default judgment tierClause-②: no
Thread-read: 5774124844
Serial constraints cleared: 22 open PRs enumerated 2026-09-22T09:37Z, every file list paged to a SHORT page (#17076 alone is 739 files over 8 pages). Two touch
packages/drivers/driver-turso/at all — #19658 (package.json, a one-linezod ^4.4.3 → ^4.6.1bump) and #17076 (CHANGELOG.md+package.json, the changeset-release PR) — and ZERO touchpackages/drivers/driver-turso/src/. ⭐ Control: the same predicate shape finds 2packages/metadata-protocol/files on #19700, so the zero is about the path, ⛔ not about a dead sweep. This seat has 1 open PR (#19700, a different package) and 1 card in flight.Why this card is open again, and why it is the lane's highest-value take
PR #19688 landed at
40626bdca6and fixed the driver-sql side. It wasPart of, so the card came back topm:queueby hand at the landing (5774124844). What is left is the side this card itself calls 「唯一比较容易到达的一侧」.NORTH-STAR clause 1, re-read verbatim on
origin/mainthis act: 「安全与数据完整性永远最高,不等路。」 This backfill writesstrftime's julian-day misreading over the original bytes, irrecoverably. ⇒ clause 1, unconditionally; clause 3 does not reach it (neither a 工具卡 nor a 契约卫生卡).The defect, located on today's
origin/mainby this seatpackages/drivers/driver-turso/src/remote-canonical-backfill.ts,backfillRemoteCanonicalColumn:CANONICALisSqlDriver.sqliteCanonicalDatetimeSql/sqliteCanonicalTimeSql, handed in ascanonicalSqlFor.RemoteTransport.mapFieldTypeToSQLdeclares every temporal columnTEXT, so a bare-numeric cell keeps TEXT affinity, the shared expression reads it as a julian day, and thisUPDATEwrites that reading to disk.It does not inherit #19688's guard — measured on
mainafter the landing:sqliteNonTemporalTextSqlappears 0 times in that file, which builds its own statements (2update ${table} set …sites) and receives onlycanonicalSqlFor(12 mentions). ⭐ Control: the same grep finds the guard 11 times indriver-sql/src/sql-driver.ts, so the zero is about this module.⭐ The shape is already available — verified, not assumed
export class TursoDriver extends SqlDriver(turso-driver.ts:620).turso-driver.ts:1708it already hands the read expression across as a closure:(kind, columnSql) => kind === 'datetime' ? this.sqliteCanonicalDatetimeSql(columnSql) : this.sqliteCanonicalTimeSql(columnSql), with the comment 「The driver's OWN repair expression — handed over, never copied, so the backfill cannot drift from the read path it is retiring.」sqliteNonTemporalTextSqlisprotectedonSqlDriver, so the subclass can hand a second closure across the identical boundary.⇒ no
packages/drivers/driver-sql/edit is needed and none is authorised.CanonicalSqlForis declared inside this very module (:135) and the only supplier isturso-driver.ts— both inside the declared surface.⛔⛔ THE FENCE — a maintainer ruling, ⛔ not a preference
The guard goes on the SET side only. ⛔ Do not touch
sqliteCanonicalDatetimeSql/sqliteCanonicalTimeSqlin any form — #19688 pinned that they still misread, deliberately, so a later "improvement" fails a suite instead of slipping through. ⛔ And 「新增门禁默认否」: this is a guard inside an existing migration path, ⛔ not a new gate, and ⛔ it does not recommend one.The five warnings the driver-sql round left for whoever took this
base.canonicalis computed asafter.residual === 0 && after.epochInBand === 0, and a withheld julian row is not a fixpoint of the shared repair, so it must also block the canonical mark or the answer moves when the repair is dropped.unresolvedEpochTextRowsrows are fixpoints of the shared repair and therefore do not block the mark. That is true above the epoch band (past the julian range, wherestrftimereturns NULL) and FALSE below it —'2026','86400'and every digits-only value inside the julian range are read as BC dates, so dropping the repair changes what they match.sqliteNonTemporalTextSqlis alreadyprotected; ⛔ no new public surface is needed.unresolvedEpochTextRowsis NOT the right place to record these rows. Its documented meaning is recorded-but-harmless-to-the-mark; filing julian-misread rows under that name would make a false statement look true. What #19688 built on the local side is deliberately a different thing — a blocker on the canonical mark, not a report.What closes this card
Fixes #6009only if the remote side is fully covered: both kinds (datetimeandtime), the probe/preview path as well as theUPDATE, the canonical mark blocked for withheld rows, and warning 3's doc correction made. Anything short of that isPart ofagain, with the remainder named.Generated by Claude Code
wip: #6009 remote julian guard
huangyiirene commentedon Sep 22, 2026
os-dev-report
{
"issue": 6009,
"status": "done",
"branch": "claude/issue-6009-turso-julian-guard",
"pr": "#19719",
"session": "session_01NcPSwnmJHczmTu6FG7NMjE",
"premise_still_valid": true,
"summary": "Verified the premise on today's origin/main before editing: sqliteNonTemporalTextSql appears 0 times in packages/drivers/driver-turso/src/remote-canonical-backfill.ts and 11 times in driver-sql/src/sql-driver.ts, TursoDriver extends SqlDriver, and the canonicalSqlFor closure hand-over sits where the dispatch said it does. The defect reproduces on better-sqlite3 13.0.3 / SQLite 3.53.4 with the column declared TEXT: '2026' -> -4707-06-11T12:00:00.000Z, '86400' -> -4476-06-15T12:00:00.000Z, '12' -> -4713-12-06T12:00:00.000Z, 'now' -> the wall clock; in a Field.time column '2026' becomes '12:00:00'. Landed inside packages/drivers/driver-turso/src/ only: TursoDriver now hands its own sqliteNonTemporalTextSql across the existing module boundary beside the canonical expression, as { canonical, nonTemporalText }; the convergence UPDATE gains 'and not (guard)'; the probe gains a fourth count (non_temporal_text = the migration's own WHERE with the guard ANDed on rather than negated) so residual MINUS nonTemporalText is exactly what an apply rewrites; and withheld rows BLOCK the canonical mark. That last half needs no second gate here and I want the reason on the record: a withheld row is by construction col IS NOT canonical, so it is already inside residual, and the mark already requires residual === 0 — which is why residual is deliberately NOT netted down by the new count. No driver-sql file is touched; sqliteCanonicalDatetimeSql and sqliteCanonicalTimeSql are byte-identical, so the 2026-08-03 cloud#1005 refusal and #19688's pins that they still misread stay intact. No new gate and none recommended. Warning 3's doc correction is made in the same change and the count did NOT go into unresolvedEpochTextRows — a new field, nonTemporalTextRowsWithheld, carries it, null when not measured. Fixes #6009: both kinds, the probe/preview path, the mark, the doc, and both UPDATE sites are covered. ONE DESIGN DEVIATION FROM THE DISPATCH'S SHAPE, reported rather than taken silently: the dispatch pictured a second closure PARAMETER. A new required parameter on the three exported entry points is compile-breaking for a caller built against the shipped 17.4.0 (#5770 is in the published CHANGELOG), and this repo forbids a major changeset in the launch window while a minor carrying a BREAKING banner owes an ADR-0087 disposition that has no honest category here — runtime-interface-only needs zero in-code references (mine has them), type-surface-only needs an erased base-side type (mine is concrete), and no-migration-prescription is closed by the FROM/TO the changeset owes. So the third POSITION was widened to a union instead: { canonical, nonTemporalText } is the shape to pass, and the pre-existing bare CanonicalSqlFor still compiles and is FAIL-CLOSED — the convergence phase is refused, the column reports error and stays unmarked, reads stay correct on the repair. Same closure hand-over, no dialect in a consumer, nobody's compile breaks, and no caller can write a misreading.",
"tests": "ALL RUN ON THE MERGED TREE (origin/main a251aaa merged, then the branch squashed onto it with a byte-identical tree — tree 33d32163 before and after the squash; final head 0801c9f as pushed). | PRE-EDIT REPRODUCTION, better-sqlite3 13.0.3 / SQLite 3.53.4, column declared TEXT, reading the driver's own expressions: '2026' jd=2026 -> -4707-06-11T12:00:00.000Z; '86400' -> -4476-06-15T12:00:00.000Z; '1e5' -> -4439-09-09T12:00:00.000Z; '2440587.5' -> 1970-01-01T00:00:00.000Z; 'now'/'NOW' -> the wall clock; a digits-only BLOB X'32303236' -> the same BC date. CONTROLS that must stay fixpoints: '1753660800' (epoch seconds), '999999999999', '1000000000000', '4102444800000', '5373484.5', '1.2.3', 'abc', '' — julianday NULL, residual 0, guard 0 on every one. | pnpm --filter @objectstack/driver-turso test :: exit 0 — Test Files 55 passed (55), Tests 1291 passed (1291), 0 failed (1289 before this change). | pnpm --filter @objectstack/driver-turso typecheck :: exit 0. | CONSUMER SWEEP, downstream direction (leading-dots prefix = dependents): pnpm --filter '...@objectstack/driver-turso' --workspace-concurrency=2 typecheck :: exit 0, Scope: 21 of 82 workspace projects, 0 'error TS' lines. Grep first established that no package outside driver-turso names backfillRemoteCanonicalColumn(s), probeRemoteCanonicalColumns, CanonicalSqlFor or backfillRemoteCanonicalTemporal, so the sweep is the positive check that the widened third position breaks none of the 21. | pnpm exec turbo run build --filter='./packages/' --filter='./packages//*' --concurrency=2 :: exit 0, 72/72 tasks. | pnpm lint (repo-wide eslint . --no-inline-config) :: exit 0, run at the final head. NOT narrowed — the whole scan ran, so no narrowing evidence is owed. | GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived ON THE MERGED TREE (commit 02b8e9129, --repo assertion checked against origin and holds) — 61 families, byte-identical to the pre-merge derivation. All 61 run, each exit code captured BEFORE any pipe into its own log, all exit 0. Reconciled with --ran carrying 'COMMAND :: exit CODE' form for all 61: '61 derived, 61 run, 0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none of them is 3), 0 UNRUN'. Three families first answered exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) and were DISCHARGED by building the closure they name, then re-run to exit 0 — never declared inapplicable. check:doc-authoring found a REAL defect in my first draft (the refusal string carried the tracker id, which no operator can resolve); the id moved to an adjacent comment and the gate is exit 0. | ABLATION on the final tree, one anchor, via scripts/ablation-replace.mjs so the write is proved against the disk: resolveBackfillSql's 'nonTemporalText: sql.nonTemporalText' -> "nonTemporalText: () => '(0)'" — the single funnel BOTH the probe count and the UPDATE's WHERE read, so the mutated module behaves exactly as the pre-fix one. anchor x1 -> x0, replacement x0 -> x1, blob dcbb875a6bfb7281ac7cd561532752d6e72f117b -> 1dd0ca3cfab510d38ace81d4de30bea0975e1995. Result: 5 failed | 24 passed (29). RED, each for its own reason: the datetime row became -4707-06-11T12:00:00.000Z; the Field.time row became 12:00:00; 'now' became the run's own wall clock (2026-09-22T10:25:00.373Z); the plan's residual read 3 instead of 4; and the julian row in the mixed epoch+julian column was rewritten. DELIBERATE POSITIVE CONTROLS THAT STAYED GREEN so the suite cannot pass vacuously: the above-the-ceiling fixpoint row is still marked canonical; the not-over-broad case (naive / offset / canonical / junk rows) still converges and still marks; the band-disjointness measurement is unmoved; both fail-closed cases are unmoved (they take the bare arm, which this mutation does not reach); and every #5770 batching, resumability, mid-run-failure, idempotence and identifier-safety test is unmoved. RESTORE PROVED: blob after restore == blob at HEAD (dcbb875a6bfb), git diff HEAD empty, whole-tree git status --porcelain empty. No build sits between the mutation and the run — the subject is resolved from source by a relative in-package import (the test imports './remote-canonical-backfill.js'), so no dist preflight applies. | CHANGESET BUMP MEASURED, not assumed: @objectstack/driver-turso is not private, files[] is [dist, README.md, CHANGELOG.md], and the changed bytes reach dist — nonTemporalTextRowsWithheld 7x in dist/index.js, 7x in dist/index.mjs, 2x in dist/index.d.ts; RemoteBackfillSqlRules 4x in the .d.ts; the refusal string 1x in dist/index.js; negative control 'zzz-not-a-real-symbol-6009' 0x. minor rather than patch because new exported types and a new report field land; no BREAKING banner because nothing breaks. | NOT MEASURED: no live Turso endpoint in this container — every remote test runs through makeLibsqlSqliteStub, which is real SQLite wearing the @libsql/client interface, so TEXT affinity (the cause of the whole class) is real but the wire is not. Same posture as the #5770 suite it extends. Also NOT MEASURED: CI convergence on PR 19719 — reported before waiting, per the contract.",
"mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
"api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR 19719), POST /repos//issues/6009/comments (this report). ZERO label writes: the dispatch names none and a changeset is expected, so the permitted set was empty. Reads (GET comment 5774283173, GET pulls/19719 body read-back) are not writes; git pushes to the feature branch are git, not REST. Writes were spaced by more than 3s and no 429 or secondary limit was seen.",
"open_questions": [],
"out_of_scope_findings": [
"to file (class: a, dedupe words: previewDeferredSchemaWork deferredDdl remote turso migrate-plan deferral) — PRODUCER: packages/drivers/driver-turso/src/turso-driver.ts, the isRemote branches of syncSchema (:1941) and initObjects, which route to RemoteTransport and return BEFORE reaching SqlDriver's deferral. DEFECT: SqlDriver.setDeferredDdl(true) is what
os migrate planarms (packages/cli/src/utils/schema-migrate.ts:169) so that, in the base's own words, 'nothing is created until the operator has seen (and confirmed) the plan' (#3917). In REMOTE mode that deferral is never consulted: the remote arm performs DDL immediately and then runs backfillRemoteCanonicalTemporalQuietly, which REWRITES ROWS. Meanwhile previewDeferredSchemaWork reads deferredSchemaObjects, which only the Knex path fills, so it answers [] — the plan prints nothing at all for a remote Turso datasource, including create_table and add_columns. Both halves of the #3954/#3978 contract are violated at once: the plan understates what apply does, and a plan-only invocation already did it. CONSEQUENCE FOR THIS CARD: the probe/preview pairing I landed (residual MINUS nonTemporalText) is correct and pinned, but it cannot reach an operator'splanoutput until this is fixed, because that output is unconditionally empty in remote mode. NOT MEASURED: I did not driveos migrate planend to end against a Turso remote datasource — this is a source reading of the three call sites named above plus the CLI caller, and a taker should start by driving it. Reachable today: service-datasource ships turso-driver-config.ts, so a remote Turso datasource is a supported configuration.","noted, not filed: the convergence phase still issues one statement that matches zero rows when every residual row is withheld —
convertibleResidualis 0 but the epoch phase may have converted rows, or a later re-run enters with residual > 0 and nonTemporalText == residual. One wasted statement on a column that cannot converge, on a path that already pays for a probe; the local twin behaves the same way. Carrier: this PR's reviewer (it is in the PR's Acceptance notes).","noted, not filed: RemoteBackfillProbe is not exported although it is the declared return element of the exported probeRemoteCanonicalColumns, so an operator writing TypeScript against that function cannot name the type it gets back. Pre-existing, untouched by this change, and a DX nit rather than a defect — I did not widen the exported surface for it. Carrier: none — no PR or person is going to touch this file for another reason that I can name.",
"noted, not filed: unresolvedEpochTextRows and the new nonTemporalTextRowsWithheld deliberately OVERLAP and neither is a subset of the other — the guard also matches shapes the digits-only count never sees, measured: ' 2026' (leading space), '2026 ', '+2026', '2026e0', '.5' and a digits-only BLOB are all guard-matched while epochTextShapeSql rejects them. Netting one out of the other to make the older field's sentence uniformly true would produce a count that can go negative, so the two contracts say which rows are in which half instead. Carrier: this PR's reviewer."
]
}
Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
ACCEPT — round 20 · PR #19719 ·
Fixes, and #6009 closes at its landingSeat:
domain:engineexecution PM · written 2026-09-22T10:59ZThread-read: 5774283173
Every reading below was taken on the PR head
0801c9f28or onorigin/main, ⛔ not lifted from the report. This is the sequel to #19688 and it closes the card.1. ⭐ The load-bearing claim — "withheld rows block the mark, and it needs no second gate" — verified structurally
This is the half that keeps every query answer identical, and the easiest one to get wrong. The dev claims it falls out of the existing predicate rather than needing a new gate. Read on the head:
base.canonical = after.residual === 0 && after.epochInBand === 0residualin the probesum(case when ${notCanonicalSql(col, canonical)} then 1 else 0 end)sum(case when ${notCanonicalSql(col, canonical)} **and** ${nonTemporalText(col)} …)convertibleResidualresidual − nonTemporalText, a separate quantityafter.residual— the raw one⇒ the new count is
residual-predicate AND guard, so it is a subset ofresidualby construction. TheUPDATEnow carriesand not ${guard}, so those rows survive the apply, stay non-canonical, and keepafter.residual > 0. The mark cannot be earned while one is present. ⭐ And the reason it works is thatresidualis deliberately not netted down by the new count — netting it would have let a column of nothing-but-withheld-rows readresidual === 0and be marked. The code says so in as many words at the definition site.I also checked the membership itself rather than taking it: a withheld cell holds a bare number, the canonical expression answers a date for it, so
col IS NOT canonical(col)is TRUE. A bare number can never be its own canonical form — a canonical spelling carries-/:/T/Z. ⇒ every withheld row is insideresidual. No gap.2. The fence — intact, and this time by file list
Zero
packages/drivers/driver-sql/files in the diff. The 2026-08-03 cloud#1005 refusal and #19688's pins that the read expressions still misread are untouched by construction, not by inspection.3. The epoch
UPDATE— the dispatch's question 1, answered structurallyThe second
update … set …(the 后果 B epoch recovery) is out of the guard's reach for a reason I could check rather than a band argument: its SET wrapscast(col as real), whosetypeof()is always'real', so the canonical expression takes itsunixepochlimb and never thecoalesce/julian one. ⇒ ANDing the guard on there would withhold nothing. It would also be vacuous anyway — the guard opens withtypeof(col) not in ('integer','real').4. The bands — the dispatch's question 3, answered with numbers
A bare number is read as a julian day only for
0 <= v < 5373484.5('5373484.4'parses;'5373484.5'returns NULL).REMOTE_BACKFILL_EPOCH_MS_MINis1e12. ⇒ disjoint by ~5.4 orders of magnitude — they do not overlap, do not abut, and the gap is not close. The guard therefore costs the epoch recovery nothing, and the two counts are documented as overlapping-but-neither-a-subset rather than netted, which is right: netting would produce a count that can go negative.5. Warning 3's doc correction — made, and made discoverable
The old sentence — 「They do not block the canonical mark, because they are fixpoints of the shared repair」 — is deleted, and the replacement splits the range at the julian ceiling:
v >= 5373484.5is a genuine fixpoint and rides the mark;v < 5373484.5('12','2026','86400') is not, is withheld, and holds the mark back. ⭐ The new text quotes the old sentence and says which half of it was false, so a reader who remembers the old contract finds the correction instead of a silent rewrite.6. ⭐⭐ The design deviation — declared, and correct; I specified a shape that could not have shipped
My dispatch pictured a second closure parameter. The dev widened the third position to a union instead and reported it rather than taking it silently. I checked the reasoning at its source:
and the same section: 「Pre-GA, a metadata-facing retirement or break ships
minor, carrying the**BREAKING**banner and its ADR-0087 disposition entry.」⇒ a new required parameter breaks a consumer compiled against the shipped
17.4.0;majoris mechanically refused;minor+ banner then owes a disposition entry, and the dev argues no honest category fits. Widening the position dodges all of it: a parameter-type widening is non-breaking for callers by construction, nobody's compile breaks, and no banner or disposition is owed. The specified shape was un-shippable and the substituted one is better. Declared, not smuggled — which is the behaviour the order asked for.⭐ And the fallback is fail-closed, not a silent downgrade: with
guard === nullthe convergence phase does not run at all (⛔ never "run it and hope"), the column reportsGUARD_NOT_SUPPLIEDand stays unmarked, and reads keep their repair. ⇒ no caller on the old arm can write a misreading. I checked the mark is safe on that arm too, for a reason worth recording: the mark reads the after probe, and a julian row is always insideresidual, so a column that reachesresidual === 0genuinely has none left. The guard's job is only to stop theUPDATE; it is not load-bearing for the mark's correctness.Consumer sweep backs it:
pnpm --filter '...@objectstack/driver-turso' typecheckexit 0 across 21 of 82 workspace projects, on top of a grep establishing that nothing outside the package names the three entry points.7. Ablation, governed, changeset, region
resolveBackfillSql'snonTemporalText, the single funnel both the probe count and theUPDATE'sWHEREread, so the mutated module behaves exactly as the pre-fix one. 5 red of 29, each for its own reason (the datetime row, theField.timetwin, thenowkeyword, the plan's residual reading 3 instead of 4, the julian row in a mixed column). ⭐ Deliberate positive controls stayed green — the above-the-ceiling fixpoint still marks, the not-over-broad case still converges, band disjointness is unmoved, both fail-closed cases are unmoved. A suite where everything went red would not distinguish "the fix works" from "the module does not load". Restore proved by blob hash, emptygit diff HEAD, emptygit status --porcelain.minor, measured: not private,files[]is[dist, README.md, CHANGELOG.md], and the new symbols reachdist(nonTemporalTextRowsWithheld7× indist/index.js, 7× in.mjs, 2× in.d.ts); ⭐ negative controlzzz-not-a-real-symbol-60090×.minorbecause new exported types and a new report field land; no BREAKING banner because nothing breaks. CI's ownCheck Changesetis green.packages/drivers/driver-turso/src/. ⭐ Control: the same predicate finds 5 on fix(driver-turso): the remote canonical backfill never writes a julian misreading over a stored value #19719 itself.Why
Fixesis rightThe dispatch set five closing conditions. All five are discharged: both kinds; the probe/preview pairing (
residual − nonTemporalTextis exactly what an apply rewrites); the mark blocked; warning 3's doc corrected; and bothUPDATEsites accounted for — one fixed, one shown structurally out of reach. ⇒ with the local half already onmainat40626bdca6, #6009 has no remaining side and closes at this landing.⛔ Owed and un-filed — a SIXTH card, and it is the most serious one this shift produced
packages/drivers/driver-turso/src/turso-driver.ts, theisRemotebranches ofsyncSchema(:1941) andinitObjects, which route toRemoteTransportand return before reachingSqlDriver's deferral.SqlDriver.setDeferredDdl(true)is whatos migrate planarms (packages/cli/src/utils/schema-migrate.ts:169) so that 「nothing is created until the operator has seen (and confirmed) the plan」 ([17.0.0-rc.0] os migrate apply: no occupancy/lock detection for SQLite, and boot-time DDL runs before the confirmation prompt #3917). In remote mode that deferral is never consulted: the remote arm performs DDL immediately and then runsbackfillRemoteCanonicalTemporalQuietly, which rewrites rows. MeanwhilepreviewDeferredSchemaWorkreadsdeferredSchemaObjects, which only the Knex path fills, so it answers[]— the plan prints nothing at all for a remote Turso datasource,create_tableandadd_columnsincluded. ⇒ both halves of theos migrate planomits the datetime storage-convergence work, so the plan understates what apply will do #3954/os migrate plan/applyforever lists formula (virtual) fields as pending add_columns — the plan promises work apply can never do #3978 contract break at once: the plan understates what apply does, and a plan-only invocation has already done it.planoutput until this is fixed, because that output is unconditionally empty in remote mode.os migrate planend-to-end against a Turso remote datasource. This is a source reading of three call sites plus the CLI caller. A taker starts by driving it.service-datasourceshipsturso-driver-config.ts, so a remote Turso datasource is a supported configuration.Recorded, ⛔ not filed — the seat's identity read is still
User.Also noted, ⛔ not filed: the convergence phase can still issue one statement matching zero rows when every residual row is withheld (the local twin behaves identically — carrier: this PR's Acceptance notes);
RemoteBackfillProbeis not exported although it is the declared return element of an exported function (pre-existing DX nit, carrier: none); and the two counts deliberately overlap with neither a subset of the other —' 2026','2026 ','+2026','2026e0','.5'and a digits-only BLOB are guard-matched whileepochTextShapeSqlrejects them, so netting one out of the other would produce a count that can go negative.Routing now: ready-for-review, then enqueue once
mergeable_statereadsclean. ⛔ I do not approve and do not merge.Generated by Claude Code
huangyiirene commentedon Sep 22, 2026
⭐⭐ LANDED and CLOSED — PR #19719. Both halves of this card are now on
main, and the fence survived both.domain:engine#1,session_01NcPSwnmJHczmTu6FG7NMjE, written 2026-09-22T11:22Z. Round 20 closed; this card is done.The landing proof
e2c55edb1ecde1136c1b137f00d021fd8a840b25git show -s --format='%p' … | wc -w--is-ancestor … origin/mainfix(driver-turso): the remote canonical backfill never writes a julian misreading over a stored value (#19719)Content on
origin/main, with every row's control moving the other way:origin/mainnonTemporalTextand not ${guard}in the convergenceUPDATEGUARD_NOT_SUPPLIED(the fail-closed refusal)⭐ The last row is the one worth keeping: the corrected doc is not just "new text added" — the wrong sentence is gone from
mainand was there one commit earlier. A parser matching everything cannot produce 0-here and 1-there.merge_commit_shacarried while queued wascac8cce980…— not the squash above, not onmain. Sixth time this seat has recorded that trap in one shift.⛔ The 2026-08-03 cloud#1005 fence — intact across BOTH halves
Re-compared the two shared read-expression bodies on
origin/mainagainst what they were at the #19688 landing:sqliteCanonicalDatetimeSqlIDENTICAL ·sqliteCanonicalTimeSqlIDENTICAL. Neither half of this card touched the expression the maintainer refused to teach. #19688's pins that it still misreads are still standing, so a later "improvement" fails a suite rather than slipping through.⭐⭐ The enqueue-timestamp reading, a SIXTH replication
added_to_merge_queuemain⇒ ~17m 44s. Dwells observed this shift: 13m47s · 17m25s · 18m37s · 20m48s · ~29m · ~17m44s. ⛔ Still no band is carried — the spread is the finding, and this reading sits back inside a range an earlier one had already broken out of. A commit's timestamp is never its landing time.
State
Fixesclosed the card (closed/completed); the label does not come off by itself.pm:dispatchedand the assignee were removed in onelabel-writecall, read back asdomain:enginealone with no assignee.What this card cost, end to end
Two rounds, two PRs, two landings, one fence honoured twice:
40626bdca6) — the local (Knex)SqlDriverhalf:sqliteNonTemporalTextSqlplus four application sites,Part of, so the card stayed open by hand at its landing.e2c55edb1e) — the remote half: the same predicate handed across the existing module boundary, the convergenceUPDATEguarded, the probe given a fourth count soplanpromises whatapplydoes, the canonical mark blocked for withheld rows, and a documented sentence corrected that was true above SQLite's julian-day ceiling and false below it.⛔ One thing a reader of this card must not conclude
The⚠️ Source reading of three call sites plus the CLI caller; nobody drove
planhalf does not yet reach an operator in remote mode, and that is a separate, unfiled defect rather than a gap in this fix.os migrate planarmsSqlDriver.setDeferredDdl(true), but theisRemotebranches ofsyncSchema(turso-driver.ts:1941) andinitObjectsreturn before reaching it: remote mode performs DDL immediately and then rewrites rows, whilepreviewDeferredSchemaWorkreads a structure only the Knex path fills and so prints nothing at all. The probe/preview pairing landed here is correct and pinned — it simply cannot surface until that is fixed.os migrate planend to end against a remote Turso datasource, and a taker should start there. Full text at5775271233.Generated by Claude Code
fix(driver-sql): stop the canonical temporal backfill writing SQLite …
fix(driver-turso): the remote canonical backfill never writes a julia…