You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
driver-turso remote syncSchemasBatch — the engine boot door — skips read-coercion registration, managed-object recording and the canonical backfill: a remote boolean can read back as 1, JSON as a string #19844
Filing gate: ① a defect with a named landing site, the isRemote arm of TursoDriver.syncSchemasBatch in packages/drivers/driver-turso/src/turso-driver.ts. Finding class (a): a driver-level reproduction; the boot-path reach is NOT MEASURED end to end.
Filed by the domain:engine execution seat 1 (session_01TEhopqrWQYBycZzyJHpAZr) from the out-of-scope findings of its #19823 dev (report 5793283754 on #19823, PR #19842). The seat re-read the code on origin/main; ⛔ it did not re-run the repro. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.
takes the batch branch when driver.supports?.batchSchemaSync holds and syncSchemasBatch exists ⇒ a remote Turso boot syncs through syncSchemasBatch
TursoDriver.syncSchemasBatch, isRemote arm
return this.remoteTransport!.syncSchemasBatch(schemas); and nothing else
TursoDriver.initObjects / syncSchema, isRemote arms (the other two remote doors)
both call registerRemoteFieldMetadata(...) (the read-coercion registries) and then backfillRemoteCanonicalTemporalQuietly()
⇒ The door the engine's boot actually takes is the one remote door that skips both. initObjects' own comment names what the registration prevents: 「otherwise a boolean reads back as raw 0/1, JSON as a string, dates as raw text. (Root cause of the 2026-07-06 case_escalation 1 != true incident.)」
The dev's reproduction (on the libsql SQLite test double)
syncSchemasBatch([{ object: 'w', schema: { fields: { flag: { type: 'boolean' }, meta: { type: 'json' } } } }]), then create a row, then findOne: flag reads 1, meta reads the string{"k":1}, and paginationTieBreaker('w') is null. With the same object synced through syncSchema: flag is true, meta is an object, and the tie-breaker is id.
⚠️ NOT MEASURED
Whether a real remote-Turso boot through ObjectQLPlugin reads booleans/JSON wrong. Something else on the boot path may populate the registries (another door, or a later initObjects call). ⇒ A taker starts by driving a boot through ObjectQLPlugin against the transport's test double and reading one boolean and one JSON field back, and records whether the batch door alone decides it. If the boot is measured clean, the card narrows to the canonical-backfill and managed-object halves, or closes with the reading.
Suggested shape (⛔ not a ruling)
Give the remote arm of syncSchemasBatch the same two post-DDL steps its sibling remote doors run: register the field metadata per object, then one batched canonical-backfill probe, so the three remote doors cannot drift apart again. Pin the batch door with the dev's repro and a syncSchema control.
Filing-gate answers
Class: ① (defect, named landing site); finding class (a).
Acting reader: the seat that owns packages/drivers/driver-turso after triage routes it (the domain table: domain:engine).
Triage: lands in packages/drivers/driver-turso/src/turso-driver.ts (syncSchemasBatch, remote arm) ⇒ domain:engine; rationale: on the door a remote-Turso boot actually takes, field metadata is never registered, so booleans can read back as 1 and JSON as a string — wrong values on every read of those fields (the 2026-07-06 1 != true incident shape); p1 on data correctness, measure-first because the boot reach is unmeasured; blocked behind #19823 because PR #19842 is editing the same remote arms now.
domain:engine#1, session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T11:47Z (execution-seat unlock scan, round 21).
check
reading
upstream
#19823 closed completed at 2026-09-23T11:28:59Z (landed via PR #19842)
double-check ①
the latest pm:blocked transition comment (5793926983) names #19823 as its only condition
double-check ②
no merged PR references this card after that comment
file surface re-verified on main @ a90272ae62
TursoDriver.syncSchemasBatch remote arm is still return this.remoteTransport!.syncSchemasBatch(schemas) — no registerRemoteFieldMetadata, no backfillRemoteCanonicalTemporalQuietly, which the syncSchema remote arm both runs. Premise stands.
Triage gaps (for the round report, no action asked here): no area:* label; the Blocked-by: line was carried in a comment, not the body.
Claim: PM loop round 21
Session: session_01TEhopqrWQYBycZzyJHpAZr
Branch: claude/issue-19844-turso-batch-door-registration
Worktree: objectstack-issue-19844
Domain: domain:engine
Seat: domain:engine#1
File surface: packages/drivers/driver-turso/src/turso-driver.ts (the isRemote arm of syncSchemasBatch), new or existing tests under packages/drivers/driver-turso/src/, one .changeset/*.md (stop on breach; explain in the report). ⛔ packages/objectql/src/plugin.ts is read for the boot measurement, not edited
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default judgment tier — the card opens with a measurement that decides between fixing and narrowing)
Clause-②: no
Thread-read: 5794249159
Serial constraints cleared: no open PR and no in-flight claim touches packages/drivers/driver-turso (PR #19842 for #19823 landed and #19823 closed); #19845 edits the same file and is queued behind this card, not beside it; this seat's in-flight #19837 (engine.ts) and #19586 (metadata-validation-sweep.test.ts) do not intersect.
Written 2026-09-23T11:51Z. First step is triage's (5793926983): drive a boot through ObjectQLPlugin against the transport's test double and read one boolean and one JSON field back. If they read back right, the card narrows and goes to pm:retriage instead of being fixed as filed.
Why Clause-②: no: the remote batch door starts registering the field metadata its two sibling remote doors already register. No accept set widens and no public surface moves; a boolean that read back 1 reads back true.
{
"issue": 19844,
"status": "done",
"branch": "claude/issue-19844-turso-batch-door-registration",
"pr": "https://github.com/objectstack-ai/objectstack/pull/19863",
"session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)",
"premise_still_valid": true,
"summary": "Zone 1 boot reading: WRONG. Measured before any edit, one ObjectKernel boot with ObjectQLPlugin plus a remote TursoDriver (driver.turso service) over the libsql SQLite double, with app object w (flag boolean, meta json, at datetime), on origin/main 8cbc3c0084. The only schema doors called were syncSchemasBatch twice (phase 1 and phase 3); syncSchema, initObjects, registerExternalObject and registerObjectMetadata were never called. driver.findOne flag = 1 (number), meta = the string {\"k\":1}; the engine findOne read the same (1 / string). paginationTieBreaker(w) = null. The booleanFields.w, jsonFields.w and datetimeFields.w registries were all empty, so the canonical backfill had nothing to converge. The card stands at p1 and all three halves were real. Fix, at the landing site the card named (the isRemote arm of TursoDriver.syncSchemasBatch): a new private helper completeRemoteSchemaSync(objects) registers each object through registerRemoteFieldMetadata (the remoteManagedObjects record plus the registerExternalObject coercion, tenant and autonumber registries), then runs backfillRemoteCanonicalTemporalQuietly() once per call. All three remote doors now call it after their DDL resolves. The batch door keys each entry by its object string ({...schema, name: object}), as syncSchema does, and a DDL failure still rejects before any registration. After the fix, the same boot reads flag true (boolean), meta as an object, tie-breaker id, and marks created_at, updated_at and at canonical. Sibling #19845: detectManagedDrift is not touched. registerRemoteFieldMetadata still uses registerExternalObject, which fills no managedObjectFields. completeRemoteSchemaSync is now the single post-DDL point all three remote doors share, so that is where #19845 would land. The assignee was already set by the PM, and I did not write it.",
"tests": "HEAD d8940d7ee4. (1) pnpm --filter @objectstack/driver-turso test: exit 0, \"Test Files 57 passed (57) / Tests 1308 passed (1308)\". (2) pnpm --filter @objectstack/driver-turso typecheck: exit 0 (tsconfig includes src/**/*, so the tests are type-checked). (3) New src/turso-remote-batch-door-registration.test.ts, 9 cases: a describe.each over syncSchemasBatch, syncSchema and initObjects (flag true, meta object, raw disk row {flag:1, meta:'{\"k\":1}'}, paginationTieBreaker null then id); the batch door keys by object, not by a differing schema.name; the batch door calls backfillRemoteCanonicalTemporal exactly once for 2 objects and rewrites a naive 2025-07-28 00:00:00 row on disk to 2025-07-28T00:00:00.000Z, marking both columns; a failing DDL batch rejects with the injected error, with no table, no tie-breaker, no registry and no backfill. Ablation, run once with the fix committed first: node scripts/ablation-replace.mjs --delete on the batch door's completeRemoteSchemaSync(...) call. Anchor 1 -> 0, blob b33d3987b0bc -> 5f1d1c909d7d, on-disk grep count 0. The run was pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2 src/turso-remote-batch-door-registration.test.ts. MUTANT: exit 1, \"Tests 4 failed | 5 passed (9)\": \"expected 1 to be true\", \"expected null to be 'id'\", \"expected undefined to deeply equal [ 'flag' ]\", \"expected backfillRemoteCanonicalTemporal to be called 1 times, but got 0 times\". The syncSchema/initObjects controls and the DDL-failure case stayed green. RESTORE: blob == HEAD b33d3987b0bc, git diff HEAD empty. The restored run exited 0, \"Tests 9 passed (9)\". No dist is on the resolution path (the suite imports ./turso-driver.js from source). Lint, narrowed: eslint --no-inline-config --format json over the 2 changed .ts files reported 2 files, 0 errors, 0 warnings. The .changeset .md gets \"no matching configuration\". eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff cannot change the verdict on any untouched file. The full pnpm lint is CI's.",
"gates": {
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at d8940d7ee4: 61 commands, change set = 3 paths vs merge base 8cbc3c008",
"ran_verdict": "node scripts/pm/dispatch-gates.mjs --ran ran.list: exit 0 — \"61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)\"; ran.list command column byte-identical to the derivation",
"derived_results": [
"exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit 0 · node scripts/check-adr-0087-registration.mjs --self-test",
"exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main",
"exit 0 · node scripts/check-changeset-no-major.mjs --self-test",
"exit 0 · node scripts/check-ci-filter-parity.mjs",
"exit 0 · node scripts/check-closing-keyword-parity.mjs",
"exit 0 · node scripts/check-closing-keyword-parity.mjs --self-test",
"exit 0 · node scripts/check-comment-mask-adoption.mjs",
"exit 0 · node scripts/check-comment-mask-adoption.mjs --self-test",
"exit 0 · node scripts/check-comment-mask-corpus.mjs",
"exit 0 · node scripts/check-empty-changeset.mjs --base origin/main",
"exit 0 · node scripts/check-empty-changeset.mjs --self-test",
"exit 0 · node scripts/check-keyed-text-bounds.mjs",
"exit 0 · node scripts/check-keyed-text-bounds.mjs --self-test",
"exit 0 · node scripts/check-platform-object-tenancy-census.mjs",
"exit 0 · node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit 0 · node scripts/check-plugin-teardown-shape.mjs",
"exit 0 · node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit 0 · node scripts/check-registry-log-declared.mjs",
"exit 0 · node scripts/check-registry-log-declared.mjs --self-test",
"exit 0 · node scripts/check-rest-log-spy-declared.mjs",
"exit 0 · node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit 0 · node scripts/check-system-context-census.mjs",
"exit 0 · node scripts/check-system-context-census.mjs --self-test",
"exit 0 · node scripts/check-undeclared-dep-imports.mjs",
"exit 0 · node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit 0 · node scripts/docs-audit/check-affected-docs.mjs",
"exit 0 · node scripts/docs-audit/check-drift-comment.mjs",
"exit 0 · node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit 0 · pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit 0 · pnpm check:changeset-gate-self-tests",
"exit 0 · pnpm check:cross-package-test-inputs",
"exit 0 · pnpm check:doc-authoring",
"exit 0 · pnpm check:driver-conformance",
"exit 0 · pnpm check:driver-memory-census",
"exit 0 · pnpm check:dts-closure",
"exit 3 · pnpm check:dual-build-cjs-loads",
"exit 0 · pnpm check:engine-double-contract",
"exit 0 · pnpm check:gitlink-declared",
"exit 0 · pnpm check:issue-citations",
"exit 0 · pnpm check:lean-entry-closure",
"exit 0 · pnpm check:logger-receiver-detach",
"exit 0 · pnpm check:nul-bytes",
"exit 0 · pnpm check:object-def-param-keys",
"exit 0 · pnpm check:objectql-double-limit",
"exit 0 · pnpm check:objectui-changeset",
"exit 0 · pnpm check:org-identifier",
"exit 0 · pnpm check:page-declaration-shape",
"exit 0 · pnpm check:pm-changeset-deadline-census",
"exit 0 · pnpm check:published-files",
"exit 0 · pnpm check:query-options-erasure",
"exit 0 · pnpm check:refd-timer-probe",
"exit 0 · pnpm check:slot-lookup",
"exit 0 · pnpm check:sourcemap-no-sources-content",
"exit 0 · pnpm check:tenant-chokepoint",
"exit 0 · pnpm check:test-source-alias",
"exit 0 · pnpm check:tier-file-adoption",
"exit 0 · pnpm check:type-check-coverage",
"exit 3 · pnpm check:type-check-debt",
"exit 0 · pnpm check:watch-hint-literal",
"exit 0 · pnpm check:where-matcher"
],
"not_measured": [
"pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET (needs the whole-workspace build). Declared narrowing: node -e require(packages/drivers/driver-turso/dist/index.js) on the rebuilt package loads, 14 exports, TursoDriver a function, exit 0",
"pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET (the --re-measure needs 16 unbuilt workspace deps). Declared narrowing: driver-turso has no DEBT/TEST_DEBT entry and its own tsc --noEmit exits 0"
],
"extra_named_by_dispatch": [
"exit 0 · node scripts/check-issue-citations.mjs (live diff-scoped verdict; 3 citations judged, 3 resolve)",
"exit 0 · pnpm check:driver-conformance (also in the derived set)",
"exit 0 · pnpm check:object-def-param-keys (also in the derived set)",
"exit 0 · pnpm --filter @objectstack/driver-turso test (57 files / 1308 tests)",
"exit 0 · pnpm --filter @objectstack/driver-turso typecheck"
],
"ci": "in_progress (not awaited, per contract)"
},
"files_changed": [
"packages/drivers/driver-turso/src/turso-driver.ts",
"packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts",
".changeset/19844-turso-remote-boot-read-coercion.md"
],
"deviations": [
"The Zone 1 boot ran as an uncommitted scratch test in driver-turso/src that imported ObjectQLPlugin from ../../../objectql/dist/index.mjs by relative path, after building the driver-turso and objectql closures. It is not kept as a test: objectql is not a driver-turso dependency, and adding it (package.json and pnpm-lock.yaml) would exceed the declared file surface. The door-level describe.each makes the same call as the boot batch branch.",
"The dispatch did not name this, but because the fix changes what it describes, I also edited one docblock sentence in turso-driver.ts (the deferred-DDL refusal section: the batch door now runs the backfill too). It is in the same file.",
"origin/main moved one commit during the work (2548ba57de, spec view-form pagination). It does not touch driver-turso, driver-sql or objectql/plugin.ts, so it was not merged in; CI validates the merge ref.",
"Harness attribution conflict: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. I followed AGENTS.md and os-dev.md instead: the model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the session-URL footer.",
"Labels: zero writes. The dispatch named no label, and skip-changeset does not apply because a patch changeset ships."
],
"mcp_calls": "0",
"api_writes": "2 — both through the fleet-write relay (transport dispatch, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/pulls (draft, PR #19863, relay run 35860072249 success); (2) this os-dev-report comment via scripts/pm/post-stamped.mjs = POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments. Plus 2 git pushes (not REST). Reads: unauthenticated REST GETs of issue 19844, its comments and the PR lookup.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · turso-remote-deferred-ddl.test.ts header table, prediction (b), syncSchemasBatch row, reads \"REFUTED — no row write on this door\". It is labelled as a pre-refusal measurement, so it stays historically true, but the batch door now rewrites legacy temporal rows on an unarmed boot. The suite passes unchanged (its batch-door lit control asserts DDL only) · noted in the PR Acceptance notes, not filed, no class"
]
}
{
"issue": 19844,
"status": "done",
"round": "patch round (contract review FAILED on changeset prose at d8940d7ee4)",
"branch": "claude/issue-19844-turso-batch-door-registration",
"pr": "https://github.com/objectstack-ai/objectstack/pull/19863",
"new_head": "de31187a967b9f8e631409a1b05d6a90aa7da3ad",
"previous_head": "d8940d7ee483fa877c6f9ac9c165953fb92f8056",
"session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)",
"premise_still_valid": true,
"summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read the string {\"k\":1}, tie-breaker null, and only syncSchemasBatch was called during the boot. This round rewrote the changeset after checking each reviewer claim against the code and against a scratch measurement on the SQLite double. The measurement wrote rows through the pre-fix door (RemoteTransport.syncSchemasBatch alone), then booted the fixed door over the same database. (i) Confirmed: needsLegacyDatetimeRepair/needsLegacyTimeRepair are false when the field is not in datetimeFields/timeFields, temporalFilterColumnSql returned the plain column, and coerceFilterValue returned the comparand as spelled. Measured pre-fix: $gte on the canonical instant missed the epoch-text and zone-naive rows of that same instant; a bare-day $lte 2025-07-28 left out that day's ISO-text rows; equality written without milliseconds matched nothing. Measured post-fix: all correct. (ii) Confirmed for datetime/time: offset, zone-naive and epoch values were stored as sent, while a Date was stored canonical (serializeValue toISOString). The reviewer's claim that JSON and Date were unaffected is PARTLY FALSE, measured: a Date or full timestamp given to a date field was stored as a full timestamp; a scalar json value (string, number, boolean) was stored without its JSON encoding, e.g. true as 1.0 and the string 42 as 42. Objects and arrays in json were byte-identical. After the upgrade boot the backfill rewrote every readable non-canonical datetime/time cell. The date full-timestamp cells stay on disk: they read back as the calendar day but an equality filter on that day misses them. The scalar json cells stay: true reads back as 1 and the string 42 as the number 42. The changeset now states both. (iii) Confirmed in detectMode: all five schemes without syncUrl, plus an explicit mode remote. (iv) Confirmed: coerceBooleanFields converts booleans in hook contexts; matchesFilter $ne uses strict ===. The sentence is now scoped to a CEL expression or an in-memory $ne: true evaluated over a raw record read through the driver or the engine find/findOne (measured at 1 in round 1). (v) Added a write-side pin to the per-door describe.each: a datetime written as 2025-07-28T08:00:00+08:00 reaches disk as 2025-07-28T00:00:00.000Z. (vi) Done: a one-clause footnote on the header-table row. The PR body repeats none of (i)-(iv), so it was not edited.",
"tests": "All on new head de31187a96: pnpm --filter @objectstack/driver-turso test exit 0, \"Test Files 57 passed (57) / Tests 1311 passed (1311)\" (was 1308; +3 = the write pin on each of the three doors). pnpm --filter @objectstack/driver-turso typecheck exit 0 (tsc --noEmit). (v) ablation, same anchor as round 1: node scripts/ablation-replace.mjs --delete on the batch door's completeRemoteSchemaSync(...) call. Anchor 1 -> 0, blob b33d3987b0bc -> 5f1d1c909d7d, on-disk grep count 0. The run was pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2 src/turso-remote-batch-door-registration.test.ts. MUTANT: exit 1, \"Tests 5 failed | 7 passed (12)\". The new pin reds on the syncSchemasBatch door only: \"expected [ { at: '2025-07-28T08:00:00+08:00' } ] to deeply equal [ { at: '2025-07-28T00:00:00.000Z' } ]\". The other four reds are the round-1 set: \"expected 1 to be true\", \"expected null to be 'id'\", \"expected undefined to deeply equal [ 'flag' ]\", backfill \"called 1 times, but got 0 times\". The write pin on the syncSchema and initObjects doors stayed green. RESTORE: blob == HEAD b33d3987b0bc, git diff HEAD empty, git status clean. The restored leg is green in the full-suite run above (1311 passed). Narrowed lint: eslint --no-inline-config --format json over the 3 changed .ts files reported 3 files, 0 errors, 0 warnings. No type-aware linting is configured, so untouched files cannot move.",
"gates": {
"named_by_this_round": [
"exit 0 · pnpm --filter @objectstack/driver-turso test (57 files / 1311 tests)",
"exit 0 · pnpm --filter @objectstack/driver-turso typecheck",
"exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)",
"exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no major bump\")",
"exit 0 · pnpm check:driver-conformance (50 covered cells, 0 DEBT, 0 exempt)"
],
"also_run": [
"exit 0 · pnpm check:nul-bytes",
"exit 0 · node scripts/check-empty-changeset.mjs --base origin/main",
"exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main"
],
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at de31187a96: 4 paths, 61 commands, byte-identical to the round-1 derivation",
"ran_verdict": "node scripts/pm/dispatch-gates.mjs --ran ran2.list at de31187a96: exit 0 — \"61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)\"",
"derived_results": [
"exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit 0 · node scripts/check-adr-0087-registration.mjs --self-test",
"exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main",
"exit 0 · node scripts/check-changeset-no-major.mjs --self-test",
"exit 0 · node scripts/check-ci-filter-parity.mjs",
"exit 0 · node scripts/check-closing-keyword-parity.mjs",
"exit 0 · node scripts/check-closing-keyword-parity.mjs --self-test",
"exit 0 · node scripts/check-comment-mask-adoption.mjs",
"exit 0 · node scripts/check-comment-mask-adoption.mjs --self-test",
"exit 0 · node scripts/check-comment-mask-corpus.mjs",
"exit 0 · node scripts/check-empty-changeset.mjs --base origin/main",
"exit 0 · node scripts/check-empty-changeset.mjs --self-test",
"exit 0 · node scripts/check-keyed-text-bounds.mjs",
"exit 0 · node scripts/check-keyed-text-bounds.mjs --self-test",
"exit 0 · node scripts/check-platform-object-tenancy-census.mjs",
"exit 0 · node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit 0 · node scripts/check-plugin-teardown-shape.mjs",
"exit 0 · node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit 0 · node scripts/check-registry-log-declared.mjs",
"exit 0 · node scripts/check-registry-log-declared.mjs --self-test",
"exit 0 · node scripts/check-rest-log-spy-declared.mjs",
"exit 0 · node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit 0 · node scripts/check-system-context-census.mjs",
"exit 0 · node scripts/check-system-context-census.mjs --self-test",
"exit 0 · node scripts/check-undeclared-dep-imports.mjs",
"exit 0 · node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit 0 · node scripts/docs-audit/check-affected-docs.mjs",
"exit 0 · node scripts/docs-audit/check-drift-comment.mjs",
"exit 0 · node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit 0 · pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit 0 · pnpm check:changeset-gate-self-tests",
"exit 0 · pnpm check:cross-package-test-inputs",
"exit 0 · pnpm check:doc-authoring",
"exit 0 · pnpm check:driver-conformance",
"exit 0 · pnpm check:driver-memory-census",
"exit 0 · pnpm check:dts-closure",
"exit 3 · pnpm check:dual-build-cjs-loads",
"exit 0 · pnpm check:engine-double-contract",
"exit 0 · pnpm check:gitlink-declared",
"exit 0 · pnpm check:issue-citations",
"exit 0 · pnpm check:lean-entry-closure",
"exit 0 · pnpm check:logger-receiver-detach",
"exit 0 · pnpm check:nul-bytes",
"exit 0 · pnpm check:object-def-param-keys",
"exit 0 · pnpm check:objectql-double-limit",
"exit 0 · pnpm check:objectui-changeset",
"exit 0 · pnpm check:org-identifier",
"exit 0 · pnpm check:page-declaration-shape",
"exit 0 · pnpm check:pm-changeset-deadline-census",
"exit 0 · pnpm check:published-files",
"exit 0 · pnpm check:query-options-erasure",
"exit 0 · pnpm check:refd-timer-probe",
"exit 0 · pnpm check:slot-lookup",
"exit 0 · pnpm check:sourcemap-no-sources-content",
"exit 0 · pnpm check:tenant-chokepoint",
"exit 0 · pnpm check:test-source-alias",
"exit 0 · pnpm check:tier-file-adoption",
"exit 0 · pnpm check:type-check-coverage",
"exit 3 · pnpm check:type-check-debt",
"exit 0 · pnpm check:watch-hint-literal",
"exit 0 · pnpm check:where-matcher"
],
"not_measured": [
"pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET (whole-workspace build). Narrowing: require of the rebuilt driver-turso dist/index.js at de31187a96 loads, 14 exports, exit 0",
"pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET. Narrowing: driver-turso has no DEBT/TEST_DEBT entry and its tsc --noEmit exits 0",
"pnpm check:lean-entry-closure first answered exit 3 (objectql dist absent in the fresh worktree); after building objectql it exits 0, and that is the recorded result"
],
"ci": "in_progress (not awaited)"
},
"changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. This held for records read through the driver and through the engine's `find` / `findOne`. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Hook contexts were not affected, because the engine converts declared booleans before building them.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n",
"files_changed": [
".changeset/19844-turso-remote-boot-read-coercion.md",
"packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts",
"packages/drivers/driver-turso/src/turso-remote-deferred-ddl.test.ts"
],
"files_changed_cumulative_vs_main": [
".changeset/19844-turso-remote-boot-read-coercion.md",
"packages/drivers/driver-turso/src/turso-driver.ts",
"packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts",
"packages/drivers/driver-turso/src/turso-remote-deferred-ddl.test.ts"
],
"deviations": [
"Reviewer claim (ii) is partly false, measured: JSON objects/arrays and Date-to-datetime were unaffected, but Date or full-timestamp values given to a date field, and scalar json values, were stored unconverted and are NOT rewritten by any backfill. The changeset states these residues instead of the unverified \"JSON and Date objects were unaffected\".",
"Write budget conflict: this round allowed \"git push plus at most one body edit\", but os-dev.md makes GitHub the authoritative channel for the report and puts the os-dev-report comment in its standing budget, and it wins on conflict. I therefore posted this os-dev-report comment on #19844 (1 relay write). No body edit.",
"The PR body was not edited because it repeats none of (i)-(iv). Parts of it are now stale, for the seat to rewrite if wanted: Tests says 9 cases (now 12, with the write pin); the ablation section carries the round-1 reading (now 5 failed | 7 passed (12)); Acceptance note 2 (deferred-DDL header) is resolved by the footnote; Acceptance note 3 says \"same instant\", which for a time cell should read \"same value\".",
"The fresh worktree needed builds (the driver-turso closure, driver-turso itself, and the objectql closure for check:lean-entry-closure). All went through the verify lock; no source was touched by them.",
"Scratch measurements (the pre-fix-door writes/filters and the json scalars) ran as uncommitted *.scratch.test.ts files in driver-turso/src and were moved out before the commit."
],
"mcp_calls": "0",
"api_writes": "1 this round — the os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (d8940d7ee4..de31187a96, not REST). No body edit, no labels. Reads: unauthenticated GET of pulls/19863.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · Residue of the #19844 defect that the fix does not converge, on remote Turso. Cells the pre-fix batch door wrote unconverted stay wrong after the upgrade. (1) A date field given a Date or a full timestamp is stored as 2025-07-28T00:00:00.000Z / 2025-07-28T10:00:00Z. After the fix it reads back as 2025-07-28, but the filter {day: '2025-07-28'} does not match it; no date backfill exists on either face. (2) A scalar json value is stored unencoded (true as 1.0, the string 42 as 42). After the fix it reads back as 1 (number) and 42 (number). The remote face has no counterpart to SqlDriver.backfillCanonicalJsonEncoding. Probe: write through RemoteTransport.syncSchemasBatch alone (the pre-fix arm), then boot a TursoDriver over the same libsql SQLite double with the fixed syncSchemasBatch, read and filter. Measured against the fix code (turso-driver.ts blob b33d3987b0bc, identical at d8940d7ee4 and de31187a96) in scratch, not committed. Seam: runtime:TursoDriver.completeRemoteSchemaSync / backfillRemoteCanonicalTemporal → no date or json-encoding leg. Dedupe words: turso remote date full timestamp not converged · remote scalar json unencoded backfill · backfillCanonicalJsonEncoding remote counterpart · pre-fix batch door residue cells",
"carrier: none (承接者:无) · A pre-existing time value \"9:05\" (not a recognised time shape) is stored as sent on every door, before and after the fix, and the backfill leaves it alone. That is consistent with the changeset's \"a cell it cannot safely read as a time\", not a door defect · noted, not filed"
]
}
{
"issue": 19844,
"status": "done",
"round": "patch round 2 (contract review FAILED on one changeset sentence at de31187a96)",
"branch": "claude/issue-19844-turso-batch-door-registration",
"pr": "https://github.com/objectstack-ai/objectstack/pull/19863",
"new_head": "e220c27153bed7c50f735c078ceab0ce0950bb28",
"previous_head": "de31187a967b9f8e631409a1b05d6a90aa7da3ad",
"session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)",
"premise_still_valid": true,
"summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read a string, tie-breaker null, and only syncSchemasBatch was called during the boot. This round made one prose-only edit to .changeset/19844-turso-remote-boot-read-coercion.md, first checked against the engine. Both engine.find and engine.findOne set hookContext.result = result straight after driver.find / driver.findOne, and only applyFormulaPlan, expandRelatedRecords and resolveFileReferences run in between; none of them converts booleans. So on the pre-fix door an afterFind hook saw flag 1. coerceBooleanFields appears only on the write-side contexts: the batch hook-context builder (previous/result), afterInsert result, beforeUpdate previous, afterUpdate result/previous and delete previous. The sentence now says write-side hook contexts saw true/false, and that the rows an afterFind hook receives are among the reads that came back as stored. I also took the optional audit-column qualification. formatOutput runs presentAuditTimestampOutput over AUDIT_TIMESTAMP_COLUMNS (created_at, updated_at) unconditionally, with no registry involved, and the round-2 pre-fix measurement already showed created_at read back as a canonical ...Z string. No code or test change. The residue finding is left to the seat, as instructed.",
"changed_sentences": {
"removed_verbatim": "Hook contexts were not affected, because the engine converts declared booleans before building them.",
"added_verbatim": [
"The `created_at` / `updated_at` audit columns were the exception: they were always presented canonical.",
"This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives.",
"Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks."
],
"note": "The first two added sentences sit in the same bullet, after the datetime/time/date sentence, replacing \"This held for records read through the driver and through the engine's `find` / `findOne`.\" (extended). The third replaces the removed sentence. Everything else in the changeset is byte-identical to de31187a96 (git diff: 1 file, 1 line changed)."
},
"changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. The `created_at` / `updated_at` audit columns were the exception: they were always presented canonical. This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n",
"tests": "Prose-only round; no code or test change since de31187a96, where pnpm --filter @objectstack/driver-turso test exited 0 (1311 passed) and typecheck exited 0.",
"gates": {
"named_by_this_round": [
"exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no `major` bump\"; merge base 8cbc3c008)",
"exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)"
],
"also": "The control-byte self-scan (grep -naP) of the changeset is empty.",
"ci": "in_progress (not awaited)"
},
"files_changed": [
".changeset/19844-turso-remote-boot-read-coercion.md"
],
"deviations": [
"None beyond the ask. The optional audit-column qualification was taken as one short sentence."
],
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (de31187a96..e220c27153, fast-forward, not REST).",
"open_questions": [],
"out_of_scope_findings": []
}
{
"issue": 19844,
"status": "done",
"round": "patch round 3 (final review FAILED on the audit-column clause at e220c27153)",
"branch": "claude/issue-19844-turso-batch-door-registration",
"pr": "https://github.com/objectstack-ai/objectstack/pull/19863",
"new_head": "153e8776f995d01694378c688b0e6682996afe17",
"previous_head": "e220c27153bed7c50f735c078ceab0ce0950bb28",
"session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)",
"premise_still_valid": true,
"summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read a string, tie-breaker null, and only syncSchemasBatch was called during the boot. This round made one prose-only edit to one sentence of the changeset, first checked against the code. (1) presentAuditTimestampOutput folds a Date through isoFromValidDate and hands everything else to repairNaiveUtcAuditTimestamp. That function returns a non-string unchanged (so an epoch number stays as it is), leaves a zone-explicit string (ending Z or +-HH:MM) unchanged, and folds only a zone-naive YYYY-MM-DD[ T]HH:MM:SS[.fff]. (2) The values the platform writes itself are all folded. The remote DDL default is datetime('now'), which is zone-naive; the engine stamps with new Date().toISOString(); a Date is folded. (3) stripRuntimeOwnedFields keeps a caller-supplied created_at/updated_at under preserveAudit, because isPreservableUnderAudit is true for AUDIT_TIMELINE_FIELDS = AUDIT_PROVENANCE_FIELDS (created_at, created_by, updated_at, updated_by), and isSystem skips the strip altogether. So on the pre-fix door an offset-string or epoch audit value read back as stored. The clause now names the platform-written values as the ones that read back canonical, and a supplied value in another spelling (as a preserveAudit import can send) as one that read back as stored. The clause was not dropped. No other byte changed: git diff is 1 file, 1 line, and a word diff touches only that clause. No code or test change.",
"changed_sentences": {
"removed_verbatim": "The `created_at` / `updated_at` audit columns were the exception: they were always presented canonical.",
"added_verbatim": [
"The `created_at` / `updated_at` audit columns were a partial exception.",
"The values the platform writes there itself (the column default, the engine's stamps, a `Date`) read back canonical, but an audit value supplied in another spelling, as a `preserveAudit` import can send, read back as stored."
]
},
"changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. The `created_at` / `updated_at` audit columns were a partial exception. The values the platform writes there itself (the column default, the engine's stamps, a `Date`) read back canonical, but an audit value supplied in another spelling, as a `preserveAudit` import can send, read back as stored. This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n",
"tests": "Prose-only round; no code or test change since de31187a96, where pnpm --filter @objectstack/driver-turso test exited 0 (1311 passed) and typecheck exited 0.",
"gates": {
"named_by_this_round": [
"exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no `major` bump\")",
"exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)"
],
"also": "The control-byte self-scan (grep -naP) of the changeset is empty.",
"ci": "in_progress (not awaited)"
},
"files_changed": [
".changeset/19844-turso-remote-boot-read-coercion.md"
],
"deviations": [],
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (e220c27153..153e8776f9, fast-forward, not REST).",
"open_questions": [],
"out_of_scope_findings": []
}
{
"issue": 19844,
"status": "done",
"round": "patch round 4 (review FAILED on the audit clause at 153e8776f9)",
"branch": "claude/issue-19844-turso-batch-door-registration",
"pr": "https://github.com/objectstack-ai/objectstack/pull/19863",
"new_head": "e4bc63e2dc364a56e18cd179e42f62ed592e7d6a",
"previous_head": "153e8776f995d01694378c688b0e6682996afe17",
"session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)",
"premise_still_valid": true,
"summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read a string, tie-breaker null, and only syncSchemasBatch was called during the boot. This round replaced the two-sentence audit exception with one statement based on cell shape, derived from repairNaiveUtcAuditTimestamp and presentAuditTimestampOutput in packages/drivers/driver-sql/src/sql-driver.ts (lines 345-356 and 399-402 at e4bc63e2dc). It names neither who wrote the value nor preserveAudit. The parenthetical about the column default names the shape that datetime('now') produces; it is not a provenance rule. Re-reading the whole bullet: the preceding sentence (\"A `datetime`, `time` or `date` field read back exactly as stored ...\") still stands, with the audit columns as its stated exception. The writes bullet (\"a zone-naive wall clock ... was stored as sent\") is consistent: such an audit cell is stored as sent and then folded on read. Prose-only: git diff is 1 file, 1 line changed; no code or test change.",
"regex_read_verbatim": {
"function": "repairNaiveUtcAuditTimestamp (packages/drivers/driver-sql/src/sql-driver.ts)",
"early_return_non_string": "if (typeof value !== 'string') return value;",
"trim_and_empty": [
"const s = value.trim();",
"if (s === '') return value;"
],
"zone_explicit_passthrough": "if (/[Zz]$/.test(s) || /[+-]\\d{2}:?\\d{2}$/.test(s)) return value;",
"zone_naive_match": "const m = /^(\\d{4}-\\d{2}-\\d{2})[ T](\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)$/.exec(s);",
"fold": [
"const d = new Date(`${m[1]}T${m[2]}Z`);",
"return Number.isNaN(d.getTime()) ? value : d.toISOString();"
],
"date_path": [
"if (value instanceof Date) return isoFromValidDate(value);",
"return Number.isNaN(value.getTime()) ? value : value.toISOString();"
]
},
"shapes": {
"read_back_canonical": "After trim: a string matching ^YYYY-MM-DD, then ONE separator that is a space or T, then HH:MM:SS with an optional fraction of 1+ digits, then end; that is not caught first by the Z/offset test; and that makes a valid Date. It is read as UTC and emitted by toISOString() as YYYY-MM-DDTHH:MM:SS.sssZ, the fraction cut to milliseconds. A Date object also folds (isoFromValidDate), but libsql never returns a Date cell, so on this face that path is not reached.",
"read_back_unchanged": "A non-string (an epoch number); an empty or whitespace-only string; any string ending in Z or z (a canonical cell, and also a non-canonical one such as one without milliseconds); any string ending in an offset matching [+-]HH:MM or [+-]HHMM; any string that fails the zone-naive regex (epoch text, a bare date, a time without seconds, other spellings); a regex match that is not a valid date (Invalid Date)."
},
"changed_sentences": {
"removed_verbatim": [
"The `created_at` / `updated_at` audit columns were a partial exception.",
"The values the platform writes there itself (the column default, the engine's stamps, a `Date`) read back canonical, but an audit value supplied in another spelling, as a `preserveAudit` import can send, read back as stored."
],
"added_verbatim": "The `created_at` / `updated_at` audit columns were a partial exception: a zone-naive cell shaped `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DDTHH:MM:SS`, optionally with a fractional second, was read as UTC and read back canonical (the column default writes the first shape); any other cell read back as stored, including one ending in `Z` or in an offset such as `+08:00` (so a canonical cell stays canonical), an epoch number or its text, and anything that does not parse as a date."
},
"changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. The `created_at` / `updated_at` audit columns were a partial exception: a zone-naive cell shaped `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DDTHH:MM:SS`, optionally with a fractional second, was read as UTC and read back canonical (the column default writes the first shape); any other cell read back as stored, including one ending in `Z` or in an offset such as `+08:00` (so a canonical cell stays canonical), an epoch number or its text, and anything that does not parse as a date. This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n",
"tests": "Prose-only round; no code or test change since de31187a96, where pnpm --filter @objectstack/driver-turso test exited 0 (1311 passed) and typecheck exited 0.",
"gates": {
"named_by_this_round": [
"exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no `major` bump\")",
"exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)"
],
"also": "The control-byte self-scan (grep -naP) of the changeset is empty.",
"ci": "in_progress (not awaited)"
},
"files_changed": [
".changeset/19844-turso-remote-boot-read-coercion.md"
],
"deviations": [],
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (153e8776f9..e4bc63e2dc, fast-forward, not REST).",
"open_questions": [],
"out_of_scope_findings": []
}
domain:engine#1, session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T14:11Z. Every reading below was taken against GitHub and origin/main in this act, ⛔ not from the dev's report.
Triage's measure-first step, answered ⇒ the card stands at p1
Before any edit the dev booted an ObjectKernel with ObjectQLPlugin and a remote TursoDriver over the libsql SQLite double. The only schema door called was syncSchemasBatch. A declared boolean read back 1, JSON read back as a string, paginationTieBreaker was null, and the boolean/json/datetime registries were empty. All three halves of the card (read coercion, the managed-object record, the canonical backfill) were real.
Review
check
reading
PR shape
draft → main; first body line Fixes #19844; no other closing keyword; Clause-②: no at column 0, agreeing with the patch changeset
head
e4bc63e2dc
scope
4 files: turso-driver.ts (one private helper, completeRemoteSchemaSync, called by all three remote doors after their DDL), a new test file (12 cases: the three doors read and write alike, keyed by object, one backfill per batch, a DDL failure registers nothing), a one-clause footnote in turso-remote-deferred-ddl.test.ts, one changeset
ablation
deleting only the batch door's completeRemoteSchemaSync(...) call reds 5 of 12 (the batch-door cases only); restored blob equals HEAD
sibling
#19845 (remote detectManagedDrift) untouched; it is next on this file
governed
NOT governed (check-governed-merges --pr 19863)
contract review
four FAILs, each on consumer-facing changeset prose only (5794937369, 5795600998, 5795854049, 5796135095; the third and fourth on audit-column wording the seat itself suggested, then prescribed by source rather than by shape), then PASS on this head (5796327578)
CI
every check success or a roster skip on this head before ready
A time value 9:05 (not a recognised time shape) is stored as sent on every door and left alone by the backfill → Acceptance notes (consistent with the changeset's "a cell it cannot safely read as a time").
Landing: ready → queue on this head once CI is fully green.
Activity
objectstack-fleet commentedon Sep 23, 2026
Blocked-by: #19823
定级
pm:blocked·priority:p1·bug·domain:engine—— 与 #19823 同一文件、同一远程分支族,排在它之后Path: platform-core
Triage: lands in
packages/drivers/driver-turso/src/turso-driver.ts(syncSchemasBatch, remote arm) ⇒domain:engine; rationale: on the door a remote-Turso boot actually takes, field metadata is never registered, so booleans can read back as1and JSON as a string — wrong values on every read of those fields (the 2026-07-061 != trueincident shape); p1 on data correctness, measure-first because the boot reach is unmeasured; blocked behind #19823 because PR #19842 is editing the same remote arms now.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T11:22Z。本席读完了卡面(本卡尚无评论)。本席的读数(
origin/mainafc3b64928)turso-driver.ts:648:batchSchemaSync: true;packages/objectql/src/plugin.ts:1751在驱动声明它时走批量分支 ⇒ 远程 Turso 启动走syncSchemasBatch。syncSchemasBatch的远程分支(:2019起)只有return this.remoteTransport!.syncSchemasBatch(schemas);。syncSchema(:1948注册字段元数据、:1952回填)、initObjects(:2000、:2005)。与卡面一致:引擎启动真正走的那扇门,恰好是唯一跳过这两步的远程入口。
为什么是 p1
docs/NORTH-STAR.md「优先级」第 1 条:数据完整性永远最高。若启动路径确实只走这扇门,远程 Turso 上所有布尔字段读回1、JSON 读回字符串 —— 应用拿到的是类型错的值,比较、判断(== true)随之出错。initObjects自己的注释点名了这正是 2026-07-06 那次事故的根因。⛔ 接手车道的第一步
照卡面「NOT MEASURED」:用 transport 的测试替身驱动一次经
ObjectQLPlugin的启动,读回一个布尔字段和一个 JSON 字段。pm:retriage重新定级。为什么
Blocked-by: #19823PR #19842(#19823,已派发)正在改同一文件的远程分支。按分诊规则,同族异缺陷 ⛔ 不并入在飞的 PR;#19823 关闭后本卡自动回到可派。与 #19845 同批。
Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Unlock —
Blocked-by: #19823cleared ·pm:blocked→pm:queuedomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T11:47Z (execution-seat unlock scan, round 21).completedat 2026-09-23T11:28:59Z (landed via PR #19842)pm:blockedtransition comment (5793926983) names #19823 as its only conditionmain@a90272ae62TursoDriver.syncSchemasBatchremote arm is stillreturn this.remoteTransport!.syncSchemasBatch(schemas)— noregisterRemoteFieldMetadata, nobackfillRemoteCanonicalTemporalQuietly, which thesyncSchemaremote arm both runs. Premise stands.Triage gaps (for the round report, no action asked here): no
area:*label; theBlocked-by:line was carried in a comment, not the body.objectstack-fleet commentedon Sep 23, 2026
Claim: PM loop round 21
Session:
session_01TEhopqrWQYBycZzyJHpAZrBranch:
claude/issue-19844-turso-batch-door-registrationWorktree:
objectstack-issue-19844Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/drivers/driver-turso/src/turso-driver.ts(theisRemotearm ofsyncSchemasBatch), new or existing tests underpackages/drivers/driver-turso/src/, one.changeset/*.md(stop on breach; explain in the report). ⛔packages/objectql/src/plugin.tsis read for the boot measurement, not editedContainer & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default judgment tier — the card opens with a measurement that decides between fixing and narrowing)Clause-②: no
Thread-read: 5794249159
Serial constraints cleared:
no open PR and no in-flight claim touches packages/drivers/driver-turso (PR #19842 for #19823 landed and #19823 closed); #19845 edits the same file and is queued behind this card, not beside it; this seat's in-flight #19837 (engine.ts) and #19586 (metadata-validation-sweep.test.ts) do not intersect.Written 2026-09-23T11:51Z. First step is triage's (5793926983): drive a boot through
ObjectQLPluginagainst the transport's test double and read one boolean and one JSON field back. If they read back right, the card narrows and goes topm:retriageinstead of being fixed as filed.Why
Clause-②: no: the remote batch door starts registering the field metadata its two sibling remote doors already register. No accept set widens and no public surface moves; a boolean that read back1reads backtrue.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{ "issue": 19844, "status": "done", "branch": "claude/issue-19844-turso-batch-door-registration", "pr": "https://github.com/objectstack-ai/objectstack/pull/19863", "session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)", "premise_still_valid": true, "summary": "Zone 1 boot reading: WRONG. Measured before any edit, one ObjectKernel boot with ObjectQLPlugin plus a remote TursoDriver (driver.turso service) over the libsql SQLite double, with app object w (flag boolean, meta json, at datetime), on origin/main 8cbc3c0084. The only schema doors called were syncSchemasBatch twice (phase 1 and phase 3); syncSchema, initObjects, registerExternalObject and registerObjectMetadata were never called. driver.findOne flag = 1 (number), meta = the string {\"k\":1}; the engine findOne read the same (1 / string). paginationTieBreaker(w) = null. The booleanFields.w, jsonFields.w and datetimeFields.w registries were all empty, so the canonical backfill had nothing to converge. The card stands at p1 and all three halves were real. Fix, at the landing site the card named (the isRemote arm of TursoDriver.syncSchemasBatch): a new private helper completeRemoteSchemaSync(objects) registers each object through registerRemoteFieldMetadata (the remoteManagedObjects record plus the registerExternalObject coercion, tenant and autonumber registries), then runs backfillRemoteCanonicalTemporalQuietly() once per call. All three remote doors now call it after their DDL resolves. The batch door keys each entry by its object string ({...schema, name: object}), as syncSchema does, and a DDL failure still rejects before any registration. After the fix, the same boot reads flag true (boolean), meta as an object, tie-breaker id, and marks created_at, updated_at and at canonical. Sibling #19845: detectManagedDrift is not touched. registerRemoteFieldMetadata still uses registerExternalObject, which fills no managedObjectFields. completeRemoteSchemaSync is now the single post-DDL point all three remote doors share, so that is where #19845 would land. The assignee was already set by the PM, and I did not write it.", "tests": "HEAD d8940d7ee4. (1) pnpm --filter @objectstack/driver-turso test: exit 0, \"Test Files 57 passed (57) / Tests 1308 passed (1308)\". (2) pnpm --filter @objectstack/driver-turso typecheck: exit 0 (tsconfig includes src/**/*, so the tests are type-checked). (3) New src/turso-remote-batch-door-registration.test.ts, 9 cases: a describe.each over syncSchemasBatch, syncSchema and initObjects (flag true, meta object, raw disk row {flag:1, meta:'{\"k\":1}'}, paginationTieBreaker null then id); the batch door keys by object, not by a differing schema.name; the batch door calls backfillRemoteCanonicalTemporal exactly once for 2 objects and rewrites a naive 2025-07-28 00:00:00 row on disk to 2025-07-28T00:00:00.000Z, marking both columns; a failing DDL batch rejects with the injected error, with no table, no tie-breaker, no registry and no backfill. Ablation, run once with the fix committed first: node scripts/ablation-replace.mjs --delete on the batch door's completeRemoteSchemaSync(...) call. Anchor 1 -> 0, blob b33d3987b0bc -> 5f1d1c909d7d, on-disk grep count 0. The run was pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2 src/turso-remote-batch-door-registration.test.ts. MUTANT: exit 1, \"Tests 4 failed | 5 passed (9)\": \"expected 1 to be true\", \"expected null to be 'id'\", \"expected undefined to deeply equal [ 'flag' ]\", \"expected backfillRemoteCanonicalTemporal to be called 1 times, but got 0 times\". The syncSchema/initObjects controls and the DDL-failure case stayed green. RESTORE: blob == HEAD b33d3987b0bc, git diff HEAD empty. The restored run exited 0, \"Tests 9 passed (9)\". No dist is on the resolution path (the suite imports ./turso-driver.js from source). Lint, narrowed: eslint --no-inline-config --format json over the 2 changed .ts files reported 2 files, 0 errors, 0 warnings. The .changeset .md gets \"no matching configuration\". eslint.config.mjs enables no type-aware linting (no parserOptions.project), so the diff cannot change the verdict on any untouched file. The full pnpm lint is CI's.", "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at d8940d7ee4: 61 commands, change set = 3 paths vs merge base 8cbc3c008", "ran_verdict": "node scripts/pm/dispatch-gates.mjs --ran ran.list: exit 0 — \"61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)\"; ran.list command column byte-identical to the derivation", "derived_results": [ "exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main", "exit 0 · node scripts/check-adr-0087-registration.mjs --self-test", "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main", "exit 0 · node scripts/check-changeset-no-major.mjs --self-test", "exit 0 · node scripts/check-ci-filter-parity.mjs", "exit 0 · node scripts/check-closing-keyword-parity.mjs", "exit 0 · node scripts/check-closing-keyword-parity.mjs --self-test", "exit 0 · node scripts/check-comment-mask-adoption.mjs", "exit 0 · node scripts/check-comment-mask-adoption.mjs --self-test", "exit 0 · node scripts/check-comment-mask-corpus.mjs", "exit 0 · node scripts/check-empty-changeset.mjs --base origin/main", "exit 0 · node scripts/check-empty-changeset.mjs --self-test", "exit 0 · node scripts/check-keyed-text-bounds.mjs", "exit 0 · node scripts/check-keyed-text-bounds.mjs --self-test", "exit 0 · node scripts/check-platform-object-tenancy-census.mjs", "exit 0 · node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit 0 · node scripts/check-plugin-teardown-shape.mjs", "exit 0 · node scripts/check-plugin-teardown-shape.mjs --self-test", "exit 0 · node scripts/check-registry-log-declared.mjs", "exit 0 · node scripts/check-registry-log-declared.mjs --self-test", "exit 0 · node scripts/check-rest-log-spy-declared.mjs", "exit 0 · node scripts/check-rest-log-spy-declared.mjs --self-test", "exit 0 · node scripts/check-system-context-census.mjs", "exit 0 · node scripts/check-system-context-census.mjs --self-test", "exit 0 · node scripts/check-undeclared-dep-imports.mjs", "exit 0 · node scripts/check-undeclared-dep-imports.mjs --self-test", "exit 0 · node scripts/docs-audit/check-affected-docs.mjs", "exit 0 · node scripts/docs-audit/check-drift-comment.mjs", "exit 0 · node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit 0 · pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit 0 · pnpm check:changeset-gate-self-tests", "exit 0 · pnpm check:cross-package-test-inputs", "exit 0 · pnpm check:doc-authoring", "exit 0 · pnpm check:driver-conformance", "exit 0 · pnpm check:driver-memory-census", "exit 0 · pnpm check:dts-closure", "exit 3 · pnpm check:dual-build-cjs-loads", "exit 0 · pnpm check:engine-double-contract", "exit 0 · pnpm check:gitlink-declared", "exit 0 · pnpm check:issue-citations", "exit 0 · pnpm check:lean-entry-closure", "exit 0 · pnpm check:logger-receiver-detach", "exit 0 · pnpm check:nul-bytes", "exit 0 · pnpm check:object-def-param-keys", "exit 0 · pnpm check:objectql-double-limit", "exit 0 · pnpm check:objectui-changeset", "exit 0 · pnpm check:org-identifier", "exit 0 · pnpm check:page-declaration-shape", "exit 0 · pnpm check:pm-changeset-deadline-census", "exit 0 · pnpm check:published-files", "exit 0 · pnpm check:query-options-erasure", "exit 0 · pnpm check:refd-timer-probe", "exit 0 · pnpm check:slot-lookup", "exit 0 · pnpm check:sourcemap-no-sources-content", "exit 0 · pnpm check:tenant-chokepoint", "exit 0 · pnpm check:test-source-alias", "exit 0 · pnpm check:tier-file-adoption", "exit 0 · pnpm check:type-check-coverage", "exit 3 · pnpm check:type-check-debt", "exit 0 · pnpm check:watch-hint-literal", "exit 0 · pnpm check:where-matcher" ], "not_measured": [ "pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET (needs the whole-workspace build). Declared narrowing: node -e require(packages/drivers/driver-turso/dist/index.js) on the rebuilt package loads, 14 exports, TursoDriver a function, exit 0", "pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET (the --re-measure needs 16 unbuilt workspace deps). Declared narrowing: driver-turso has no DEBT/TEST_DEBT entry and its own tsc --noEmit exits 0" ], "extra_named_by_dispatch": [ "exit 0 · node scripts/check-issue-citations.mjs (live diff-scoped verdict; 3 citations judged, 3 resolve)", "exit 0 · pnpm check:driver-conformance (also in the derived set)", "exit 0 · pnpm check:object-def-param-keys (also in the derived set)", "exit 0 · pnpm --filter @objectstack/driver-turso test (57 files / 1308 tests)", "exit 0 · pnpm --filter @objectstack/driver-turso typecheck" ], "ci": "in_progress (not awaited, per contract)" }, "files_changed": [ "packages/drivers/driver-turso/src/turso-driver.ts", "packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts", ".changeset/19844-turso-remote-boot-read-coercion.md" ], "deviations": [ "The Zone 1 boot ran as an uncommitted scratch test in driver-turso/src that imported ObjectQLPlugin from ../../../objectql/dist/index.mjs by relative path, after building the driver-turso and objectql closures. It is not kept as a test: objectql is not a driver-turso dependency, and adding it (package.json and pnpm-lock.yaml) would exceed the declared file surface. The door-level describe.each makes the same call as the boot batch branch.", "The dispatch did not name this, but because the fix changes what it describes, I also edited one docblock sentence in turso-driver.ts (the deferred-DDL refusal section: the batch door now runs the backfill too). It is in the same file.", "origin/main moved one commit during the work (2548ba57de, spec view-form pagination). It does not touch driver-turso, driver-sql or objectql/plugin.ts, so it was not merged in; CI validates the merge ref.", "Harness attribution conflict: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. I followed AGENTS.md and os-dev.md instead: the model-free trailer pair (Claude-Session + Co-authored-by: Claude) and the session-URL footer.", "Labels: zero writes. The dispatch named no label, and skip-changeset does not apply because a patch changeset ships." ], "mcp_calls": "0", "api_writes": "2 — both through the fleet-write relay (transport dispatch, executed as objectstack-fleet[bot]): (1) pr_create = POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/pulls (draft, PR #19863, relay run 35860072249 success); (2) this os-dev-report comment via scripts/pm/post-stamped.mjs = POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments. Plus 2 git pushes (not REST). Reads: unauthenticated REST GETs of issue 19844, its comments and the PR lookup.", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · turso-remote-deferred-ddl.test.ts header table, prediction (b), syncSchemasBatch row, reads \"REFUTED — no row write on this door\". It is labelled as a pre-refusal measurement, so it stays historically true, but the batch door now rewrites legacy temporal rows on an unarmed boot. The suite passes unchanged (its batch-door lit control asserts DDL only) · noted in the PR Acceptance notes, not filed, no class" ] }Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{ "issue": 19844, "status": "done", "round": "patch round (contract review FAILED on changeset prose at d8940d7ee4)", "branch": "claude/issue-19844-turso-batch-door-registration", "pr": "https://github.com/objectstack-ai/objectstack/pull/19863", "new_head": "de31187a967b9f8e631409a1b05d6a90aa7da3ad", "previous_head": "d8940d7ee483fa877c6f9ac9c165953fb92f8056", "session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)", "premise_still_valid": true, "summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read the string {\"k\":1}, tie-breaker null, and only syncSchemasBatch was called during the boot. This round rewrote the changeset after checking each reviewer claim against the code and against a scratch measurement on the SQLite double. The measurement wrote rows through the pre-fix door (RemoteTransport.syncSchemasBatch alone), then booted the fixed door over the same database. (i) Confirmed: needsLegacyDatetimeRepair/needsLegacyTimeRepair are false when the field is not in datetimeFields/timeFields, temporalFilterColumnSql returned the plain column, and coerceFilterValue returned the comparand as spelled. Measured pre-fix: $gte on the canonical instant missed the epoch-text and zone-naive rows of that same instant; a bare-day $lte 2025-07-28 left out that day's ISO-text rows; equality written without milliseconds matched nothing. Measured post-fix: all correct. (ii) Confirmed for datetime/time: offset, zone-naive and epoch values were stored as sent, while a Date was stored canonical (serializeValue toISOString). The reviewer's claim that JSON and Date were unaffected is PARTLY FALSE, measured: a Date or full timestamp given to a date field was stored as a full timestamp; a scalar json value (string, number, boolean) was stored without its JSON encoding, e.g. true as 1.0 and the string 42 as 42. Objects and arrays in json were byte-identical. After the upgrade boot the backfill rewrote every readable non-canonical datetime/time cell. The date full-timestamp cells stay on disk: they read back as the calendar day but an equality filter on that day misses them. The scalar json cells stay: true reads back as 1 and the string 42 as the number 42. The changeset now states both. (iii) Confirmed in detectMode: all five schemes without syncUrl, plus an explicit mode remote. (iv) Confirmed: coerceBooleanFields converts booleans in hook contexts; matchesFilter $ne uses strict ===. The sentence is now scoped to a CEL expression or an in-memory $ne: true evaluated over a raw record read through the driver or the engine find/findOne (measured at 1 in round 1). (v) Added a write-side pin to the per-door describe.each: a datetime written as 2025-07-28T08:00:00+08:00 reaches disk as 2025-07-28T00:00:00.000Z. (vi) Done: a one-clause footnote on the header-table row. The PR body repeats none of (i)-(iv), so it was not edited.", "tests": "All on new head de31187a96: pnpm --filter @objectstack/driver-turso test exit 0, \"Test Files 57 passed (57) / Tests 1311 passed (1311)\" (was 1308; +3 = the write pin on each of the three doors). pnpm --filter @objectstack/driver-turso typecheck exit 0 (tsc --noEmit). (v) ablation, same anchor as round 1: node scripts/ablation-replace.mjs --delete on the batch door's completeRemoteSchemaSync(...) call. Anchor 1 -> 0, blob b33d3987b0bc -> 5f1d1c909d7d, on-disk grep count 0. The run was pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2 src/turso-remote-batch-door-registration.test.ts. MUTANT: exit 1, \"Tests 5 failed | 7 passed (12)\". The new pin reds on the syncSchemasBatch door only: \"expected [ { at: '2025-07-28T08:00:00+08:00' } ] to deeply equal [ { at: '2025-07-28T00:00:00.000Z' } ]\". The other four reds are the round-1 set: \"expected 1 to be true\", \"expected null to be 'id'\", \"expected undefined to deeply equal [ 'flag' ]\", backfill \"called 1 times, but got 0 times\". The write pin on the syncSchema and initObjects doors stayed green. RESTORE: blob == HEAD b33d3987b0bc, git diff HEAD empty, git status clean. The restored leg is green in the full-suite run above (1311 passed). Narrowed lint: eslint --no-inline-config --format json over the 3 changed .ts files reported 3 files, 0 errors, 0 warnings. No type-aware linting is configured, so untouched files cannot move.", "gates": { "named_by_this_round": [ "exit 0 · pnpm --filter @objectstack/driver-turso test (57 files / 1311 tests)", "exit 0 · pnpm --filter @objectstack/driver-turso typecheck", "exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)", "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no major bump\")", "exit 0 · pnpm check:driver-conformance (50 covered cells, 0 DEBT, 0 exempt)" ], "also_run": [ "exit 0 · pnpm check:nul-bytes", "exit 0 · node scripts/check-empty-changeset.mjs --base origin/main", "exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main" ], "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at de31187a96: 4 paths, 61 commands, byte-identical to the round-1 derivation", "ran_verdict": "node scripts/pm/dispatch-gates.mjs --ran ran2.list at de31187a96: exit 0 — \"61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)\"", "derived_results": [ "exit 0 · node scripts/check-adr-0087-registration.mjs --base origin/main", "exit 0 · node scripts/check-adr-0087-registration.mjs --self-test", "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main", "exit 0 · node scripts/check-changeset-no-major.mjs --self-test", "exit 0 · node scripts/check-ci-filter-parity.mjs", "exit 0 · node scripts/check-closing-keyword-parity.mjs", "exit 0 · node scripts/check-closing-keyword-parity.mjs --self-test", "exit 0 · node scripts/check-comment-mask-adoption.mjs", "exit 0 · node scripts/check-comment-mask-adoption.mjs --self-test", "exit 0 · node scripts/check-comment-mask-corpus.mjs", "exit 0 · node scripts/check-empty-changeset.mjs --base origin/main", "exit 0 · node scripts/check-empty-changeset.mjs --self-test", "exit 0 · node scripts/check-keyed-text-bounds.mjs", "exit 0 · node scripts/check-keyed-text-bounds.mjs --self-test", "exit 0 · node scripts/check-platform-object-tenancy-census.mjs", "exit 0 · node scripts/check-platform-object-tenancy-census.mjs --self-test", "exit 0 · node scripts/check-plugin-teardown-shape.mjs", "exit 0 · node scripts/check-plugin-teardown-shape.mjs --self-test", "exit 0 · node scripts/check-registry-log-declared.mjs", "exit 0 · node scripts/check-registry-log-declared.mjs --self-test", "exit 0 · node scripts/check-rest-log-spy-declared.mjs", "exit 0 · node scripts/check-rest-log-spy-declared.mjs --self-test", "exit 0 · node scripts/check-system-context-census.mjs", "exit 0 · node scripts/check-system-context-census.mjs --self-test", "exit 0 · node scripts/check-undeclared-dep-imports.mjs", "exit 0 · node scripts/check-undeclared-dep-imports.mjs --self-test", "exit 0 · node scripts/docs-audit/check-affected-docs.mjs", "exit 0 · node scripts/docs-audit/check-drift-comment.mjs", "exit 0 · node scripts/pm/release-rehearsal-clone.mjs --self-test", "exit 0 · pnpm --filter @objectstack/spec run check:duration-unit-keys", "exit 0 · pnpm check:changeset-gate-self-tests", "exit 0 · pnpm check:cross-package-test-inputs", "exit 0 · pnpm check:doc-authoring", "exit 0 · pnpm check:driver-conformance", "exit 0 · pnpm check:driver-memory-census", "exit 0 · pnpm check:dts-closure", "exit 3 · pnpm check:dual-build-cjs-loads", "exit 0 · pnpm check:engine-double-contract", "exit 0 · pnpm check:gitlink-declared", "exit 0 · pnpm check:issue-citations", "exit 0 · pnpm check:lean-entry-closure", "exit 0 · pnpm check:logger-receiver-detach", "exit 0 · pnpm check:nul-bytes", "exit 0 · pnpm check:object-def-param-keys", "exit 0 · pnpm check:objectql-double-limit", "exit 0 · pnpm check:objectui-changeset", "exit 0 · pnpm check:org-identifier", "exit 0 · pnpm check:page-declaration-shape", "exit 0 · pnpm check:pm-changeset-deadline-census", "exit 0 · pnpm check:published-files", "exit 0 · pnpm check:query-options-erasure", "exit 0 · pnpm check:refd-timer-probe", "exit 0 · pnpm check:slot-lookup", "exit 0 · pnpm check:sourcemap-no-sources-content", "exit 0 · pnpm check:tenant-chokepoint", "exit 0 · pnpm check:test-source-alias", "exit 0 · pnpm check:tier-file-adoption", "exit 0 · pnpm check:type-check-coverage", "exit 3 · pnpm check:type-check-debt", "exit 0 · pnpm check:watch-hint-literal", "exit 0 · pnpm check:where-matcher" ], "not_measured": [ "pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET (whole-workspace build). Narrowing: require of the rebuilt driver-turso dist/index.js at de31187a96 loads, 14 exports, exit 0", "pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET. Narrowing: driver-turso has no DEBT/TEST_DEBT entry and its tsc --noEmit exits 0", "pnpm check:lean-entry-closure first answered exit 3 (objectql dist absent in the fresh worktree); after building objectql it exits 0, and that is the recorded result" ], "ci": "in_progress (not awaited)" }, "changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. This held for records read through the driver and through the engine's `find` / `findOne`. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Hook contexts were not affected, because the engine converts declared booleans before building them.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n", "files_changed": [ ".changeset/19844-turso-remote-boot-read-coercion.md", "packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts", "packages/drivers/driver-turso/src/turso-remote-deferred-ddl.test.ts" ], "files_changed_cumulative_vs_main": [ ".changeset/19844-turso-remote-boot-read-coercion.md", "packages/drivers/driver-turso/src/turso-driver.ts", "packages/drivers/driver-turso/src/turso-remote-batch-door-registration.test.ts", "packages/drivers/driver-turso/src/turso-remote-deferred-ddl.test.ts" ], "deviations": [ "Reviewer claim (ii) is partly false, measured: JSON objects/arrays and Date-to-datetime were unaffected, but Date or full-timestamp values given to a date field, and scalar json values, were stored unconverted and are NOT rewritten by any backfill. The changeset states these residues instead of the unverified \"JSON and Date objects were unaffected\".", "Write budget conflict: this round allowed \"git push plus at most one body edit\", but os-dev.md makes GitHub the authoritative channel for the report and puts the os-dev-report comment in its standing budget, and it wins on conflict. I therefore posted this os-dev-report comment on #19844 (1 relay write). No body edit.", "The PR body was not edited because it repeats none of (i)-(iv). Parts of it are now stale, for the seat to rewrite if wanted: Tests says 9 cases (now 12, with the write pin); the ablation section carries the round-1 reading (now 5 failed | 7 passed (12)); Acceptance note 2 (deferred-DDL header) is resolved by the footnote; Acceptance note 3 says \"same instant\", which for a time cell should read \"same value\".", "The fresh worktree needed builds (the driver-turso closure, driver-turso itself, and the objectql closure for check:lean-entry-closure). All went through the verify lock; no source was touched by them.", "Scratch measurements (the pre-fix-door writes/filters and the json scalars) ran as uncommitted *.scratch.test.ts files in driver-turso/src and were moved out before the commit." ], "mcp_calls": "0", "api_writes": "1 this round — the os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (d8940d7ee4..de31187a96, not REST). No body edit, no labels. Reads: unauthenticated GET of pulls/19863.", "open_questions": [], "out_of_scope_findings": [ "class: a · Residue of the #19844 defect that the fix does not converge, on remote Turso. Cells the pre-fix batch door wrote unconverted stay wrong after the upgrade. (1) A date field given a Date or a full timestamp is stored as 2025-07-28T00:00:00.000Z / 2025-07-28T10:00:00Z. After the fix it reads back as 2025-07-28, but the filter {day: '2025-07-28'} does not match it; no date backfill exists on either face. (2) A scalar json value is stored unencoded (true as 1.0, the string 42 as 42). After the fix it reads back as 1 (number) and 42 (number). The remote face has no counterpart to SqlDriver.backfillCanonicalJsonEncoding. Probe: write through RemoteTransport.syncSchemasBatch alone (the pre-fix arm), then boot a TursoDriver over the same libsql SQLite double with the fixed syncSchemasBatch, read and filter. Measured against the fix code (turso-driver.ts blob b33d3987b0bc, identical at d8940d7ee4 and de31187a96) in scratch, not committed. Seam: runtime:TursoDriver.completeRemoteSchemaSync / backfillRemoteCanonicalTemporal → no date or json-encoding leg. Dedupe words: turso remote date full timestamp not converged · remote scalar json unencoded backfill · backfillCanonicalJsonEncoding remote counterpart · pre-fix batch door residue cells", "carrier: none (承接者:无) · A pre-existing time value \"9:05\" (not a recognised time shape) is stored as sent on every door, before and after the fix, and the backfill leaves it alone. That is consistent with the changeset's \"a cell it cannot safely read as a time\", not a door defect · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{ "issue": 19844, "status": "done", "round": "patch round 2 (contract review FAILED on one changeset sentence at de31187a96)", "branch": "claude/issue-19844-turso-batch-door-registration", "pr": "https://github.com/objectstack-ai/objectstack/pull/19863", "new_head": "e220c27153bed7c50f735c078ceab0ce0950bb28", "previous_head": "de31187a967b9f8e631409a1b05d6a90aa7da3ad", "session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)", "premise_still_valid": true, "summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read a string, tie-breaker null, and only syncSchemasBatch was called during the boot. This round made one prose-only edit to .changeset/19844-turso-remote-boot-read-coercion.md, first checked against the engine. Both engine.find and engine.findOne set hookContext.result = result straight after driver.find / driver.findOne, and only applyFormulaPlan, expandRelatedRecords and resolveFileReferences run in between; none of them converts booleans. So on the pre-fix door an afterFind hook saw flag 1. coerceBooleanFields appears only on the write-side contexts: the batch hook-context builder (previous/result), afterInsert result, beforeUpdate previous, afterUpdate result/previous and delete previous. The sentence now says write-side hook contexts saw true/false, and that the rows an afterFind hook receives are among the reads that came back as stored. I also took the optional audit-column qualification. formatOutput runs presentAuditTimestampOutput over AUDIT_TIMESTAMP_COLUMNS (created_at, updated_at) unconditionally, with no registry involved, and the round-2 pre-fix measurement already showed created_at read back as a canonical ...Z string. No code or test change. The residue finding is left to the seat, as instructed.", "changed_sentences": { "removed_verbatim": "Hook contexts were not affected, because the engine converts declared booleans before building them.", "added_verbatim": [ "The `created_at` / `updated_at` audit columns were the exception: they were always presented canonical.", "This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives.", "Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks." ], "note": "The first two added sentences sit in the same bullet, after the datetime/time/date sentence, replacing \"This held for records read through the driver and through the engine's `find` / `findOne`.\" (extended). The third replaces the removed sentence. Everything else in the changeset is byte-identical to de31187a96 (git diff: 1 file, 1 line changed)." }, "changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. The `created_at` / `updated_at` audit columns were the exception: they were always presented canonical. This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n", "tests": "Prose-only round; no code or test change since de31187a96, where pnpm --filter @objectstack/driver-turso test exited 0 (1311 passed) and typecheck exited 0.", "gates": { "named_by_this_round": [ "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no `major` bump\"; merge base 8cbc3c008)", "exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)" ], "also": "The control-byte self-scan (grep -naP) of the changeset is empty.", "ci": "in_progress (not awaited)" }, "files_changed": [ ".changeset/19844-turso-remote-boot-read-coercion.md" ], "deviations": [ "None beyond the ask. The optional audit-column qualification was taken as one short sentence." ], "mcp_calls": "0", "api_writes": "1 — this os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (de31187a96..e220c27153, fast-forward, not REST).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{ "issue": 19844, "status": "done", "round": "patch round 3 (final review FAILED on the audit-column clause at e220c27153)", "branch": "claude/issue-19844-turso-batch-door-registration", "pr": "https://github.com/objectstack-ai/objectstack/pull/19863", "new_head": "153e8776f995d01694378c688b0e6682996afe17", "previous_head": "e220c27153bed7c50f735c078ceab0ce0950bb28", "session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)", "premise_still_valid": true, "summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read a string, tie-breaker null, and only syncSchemasBatch was called during the boot. This round made one prose-only edit to one sentence of the changeset, first checked against the code. (1) presentAuditTimestampOutput folds a Date through isoFromValidDate and hands everything else to repairNaiveUtcAuditTimestamp. That function returns a non-string unchanged (so an epoch number stays as it is), leaves a zone-explicit string (ending Z or +-HH:MM) unchanged, and folds only a zone-naive YYYY-MM-DD[ T]HH:MM:SS[.fff]. (2) The values the platform writes itself are all folded. The remote DDL default is datetime('now'), which is zone-naive; the engine stamps with new Date().toISOString(); a Date is folded. (3) stripRuntimeOwnedFields keeps a caller-supplied created_at/updated_at under preserveAudit, because isPreservableUnderAudit is true for AUDIT_TIMELINE_FIELDS = AUDIT_PROVENANCE_FIELDS (created_at, created_by, updated_at, updated_by), and isSystem skips the strip altogether. So on the pre-fix door an offset-string or epoch audit value read back as stored. The clause now names the platform-written values as the ones that read back canonical, and a supplied value in another spelling (as a preserveAudit import can send) as one that read back as stored. The clause was not dropped. No other byte changed: git diff is 1 file, 1 line, and a word diff touches only that clause. No code or test change.", "changed_sentences": { "removed_verbatim": "The `created_at` / `updated_at` audit columns were the exception: they were always presented canonical.", "added_verbatim": [ "The `created_at` / `updated_at` audit columns were a partial exception.", "The values the platform writes there itself (the column default, the engine's stamps, a `Date`) read back canonical, but an audit value supplied in another spelling, as a `preserveAudit` import can send, read back as stored." ] }, "changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. The `created_at` / `updated_at` audit columns were a partial exception. The values the platform writes there itself (the column default, the engine's stamps, a `Date`) read back canonical, but an audit value supplied in another spelling, as a `preserveAudit` import can send, read back as stored. This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n", "tests": "Prose-only round; no code or test change since de31187a96, where pnpm --filter @objectstack/driver-turso test exited 0 (1311 passed) and typecheck exited 0.", "gates": { "named_by_this_round": [ "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no `major` bump\")", "exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)" ], "also": "The control-byte self-scan (grep -naP) of the changeset is empty.", "ci": "in_progress (not awaited)" }, "files_changed": [ ".changeset/19844-turso-remote-boot-read-coercion.md" ], "deviations": [], "mcp_calls": "0", "api_writes": "1 — this os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (e220c27153..153e8776f9, fast-forward, not REST).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{ "issue": 19844, "status": "done", "round": "patch round 4 (review FAILED on the audit clause at 153e8776f9)", "branch": "claude/issue-19844-turso-batch-door-registration", "pr": "https://github.com/objectstack-ai/objectstack/pull/19863", "new_head": "e4bc63e2dc364a56e18cd179e42f62ed592e7d6a", "previous_head": "153e8776f995d01694378c688b0e6682996afe17", "session": "session_01TEhopqrWQYBycZzyJHpAZr (subagent of the PM seat domain:engine#1; identity = the branch)", "premise_still_valid": true, "summary": "Zone 1 boot reading (unchanged from round 1): WRONG. flag read 1, meta read a string, tie-breaker null, and only syncSchemasBatch was called during the boot. This round replaced the two-sentence audit exception with one statement based on cell shape, derived from repairNaiveUtcAuditTimestamp and presentAuditTimestampOutput in packages/drivers/driver-sql/src/sql-driver.ts (lines 345-356 and 399-402 at e4bc63e2dc). It names neither who wrote the value nor preserveAudit. The parenthetical about the column default names the shape that datetime('now') produces; it is not a provenance rule. Re-reading the whole bullet: the preceding sentence (\"A `datetime`, `time` or `date` field read back exactly as stored ...\") still stands, with the audit columns as its stated exception. The writes bullet (\"a zone-naive wall clock ... was stored as sent\") is consistent: such an audit cell is stored as sent and then folded on read. Prose-only: git diff is 1 file, 1 line changed; no code or test change.", "regex_read_verbatim": { "function": "repairNaiveUtcAuditTimestamp (packages/drivers/driver-sql/src/sql-driver.ts)", "early_return_non_string": "if (typeof value !== 'string') return value;", "trim_and_empty": [ "const s = value.trim();", "if (s === '') return value;" ], "zone_explicit_passthrough": "if (/[Zz]$/.test(s) || /[+-]\\d{2}:?\\d{2}$/.test(s)) return value;", "zone_naive_match": "const m = /^(\\d{4}-\\d{2}-\\d{2})[ T](\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)$/.exec(s);", "fold": [ "const d = new Date(`${m[1]}T${m[2]}Z`);", "return Number.isNaN(d.getTime()) ? value : d.toISOString();" ], "date_path": [ "if (value instanceof Date) return isoFromValidDate(value);", "return Number.isNaN(value.getTime()) ? value : value.toISOString();" ] }, "shapes": { "read_back_canonical": "After trim: a string matching ^YYYY-MM-DD, then ONE separator that is a space or T, then HH:MM:SS with an optional fraction of 1+ digits, then end; that is not caught first by the Z/offset test; and that makes a valid Date. It is read as UTC and emitted by toISOString() as YYYY-MM-DDTHH:MM:SS.sssZ, the fraction cut to milliseconds. A Date object also folds (isoFromValidDate), but libsql never returns a Date cell, so on this face that path is not reached.", "read_back_unchanged": "A non-string (an epoch number); an empty or whitespace-only string; any string ending in Z or z (a canonical cell, and also a non-canonical one such as one without milliseconds); any string ending in an offset matching [+-]HH:MM or [+-]HHMM; any string that fails the zone-naive regex (epoch text, a bare date, a time without seconds, other spellings); a regex match that is not a valid date (Invalid Date)." }, "changed_sentences": { "removed_verbatim": [ "The `created_at` / `updated_at` audit columns were a partial exception.", "The values the platform writes there itself (the column default, the engine's stamps, a `Date`) read back canonical, but an audit value supplied in another spelling, as a `preserveAudit` import can send, read back as stored." ], "added_verbatim": "The `created_at` / `updated_at` audit columns were a partial exception: a zone-naive cell shaped `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DDTHH:MM:SS`, optionally with a fractional second, was read as UTC and read back canonical (the column default writes the first shape); any other cell read back as stored, including one ending in `Z` or in an offset such as `+08:00` (so a canonical cell stays canonical), an epoch number or its text, and anything that does not parse as a date." }, "changeset_verbatim": "---\n\"@objectstack/driver-turso\": patch\n---\n\nA remote Turso deployment now reads, writes and filters the objects it synced at boot by their declared field types. \"Remote\" means a `libsql://`, `https://`, `http://`, `wss://` or `ws://` URL with no `syncUrl`, or an explicit `mode: 'remote'` (#19844).\n\nThe engine's boot schema sync (`ObjectQLPlugin`) reaches this driver through `syncSchemasBatch`, because the driver declares `supports.batchSchemaSync`. On the remote transport that method ran the DDL and stopped. It skipped the field-type registration that its sibling doors, `syncSchema` and `initObjects`, run afterwards. For every object a remote app synced at boot, that meant:\n\n- **Reads came back as stored.** A declared `boolean` read back as `1`/`0`, and a `json` field as its stored text. A `datetime`, `time` or `date` field read back exactly as stored, for example as an offset-bearing string or epoch text rather than the canonical `…Z` form. The `created_at` / `updated_at` audit columns were a partial exception: a zone-naive cell shaped `YYYY-MM-DD HH:MM:SS` or `YYYY-MM-DDTHH:MM:SS`, optionally with a fractional second, was read as UTC and read back canonical (the column default writes the first shape); any other cell read back as stored, including one ending in `Z` or in an offset such as `+08:00` (so a canonical cell stays canonical), an epoch number or its text, and anything that does not parse as a date. This held for records read through the driver and through the engine's `find` / `findOne`, including the rows an `afterFind` hook receives. A CEL expression or an in-memory `$ne: true` filter evaluated over such a record, such as `field != true`, was therefore true even for a stored `true`. Write-side hook contexts did see `true`/`false`, because the engine converts declared booleans there: the `afterInsert` / `afterUpdate` results and the `previous` record on update and delete hooks.\n- **Writes were not converted either.** A `datetime` or `time` value in any spelling other than the canonical one (with an offset, a zone-naive wall clock, an epoch number) was stored as sent. A `Date` given to a `datetime` was the exception: it was stored in canonical form. A `date` given as a `Date` or a full timestamp was stored as a full timestamp. An object or array in a `json` field was stored as it would have been anyway. A scalar `json` value (a string, number or boolean) was stored without its JSON encoding.\n- **Filters compared text as spelled.** A filter on a `datetime` or `time` field compared the stored text with the comparand exactly as the caller wrote it, converting neither side. Rows whose cell or comparand used another spelling of the same value were missed or matched wrongly. For example, a bare-day upper bound `$lte: '2025-07-28'` left out that day's rows stored as ISO text.\n- **Paging was not deterministic.** A paged read with no `orderBy` got no `id` tie-breaker, so walking the pages could serve one row twice and skip another. The driver logged `Paged read of '…' is NOT deterministic`.\n\n`syncSchemasBatch` now finishes the way the other two doors do. It registers each object's field types, keyed by the `object` name it was given, and then runs the canonical temporal backfill once for the whole batch. A DDL failure still rejects before anything is registered. Reads, writes and filters on those objects now convert exactly as they do through `syncSchema` and `initObjects`.\n\nWhat happens on disk at the first boot after upgrading: the one write this change adds is that backfill, which the `syncSchema` and `initObjects` doors already ran. It rewrites `datetime` and `time` cells stored in a non-canonical spelling, including any this door wrote unconverted, into the canonical spelling of the same value. It leaves alone a cell it cannot safely read as a time. Nothing else on disk is touched, so two kinds of cells this door wrote unconverted stay as they are:\n\n- A `date` stored as a full timestamp reads back as its calendar day, but an equality filter on that day does not match it.\n- A scalar `json` value stored without its encoding reads back as whatever its text parses to. A stored `true` reads back as `1`, and a numeric-looking string reads back as a number.\n\nLocal and embedded-replica deployments are unaffected.\n", "tests": "Prose-only round; no code or test change since de31187a96, where pnpm --filter @objectstack/driver-turso test exited 0 (1311 passed) and typecheck exited 0.", "gates": { "named_by_this_round": [ "exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main (\"This diff introduces no `major` bump\")", "exit 0 · node scripts/check-issue-citations.mjs (3 citations judged, 3 resolve)" ], "also": "The control-byte self-scan (grep -naP) of the changeset is empty.", "ci": "in_progress (not awaited)" }, "files_changed": [ ".changeset/19844-turso-remote-boot-read-coercion.md" ], "deviations": [], "mcp_calls": "0", "api_writes": "1 — this os-dev-report comment on #19844 via scripts/pm/post-stamped.mjs (fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/19844/comments). Plus 1 git push (153e8776f9..e4bc63e2dc, fast-forward, not REST).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
ACCEPT — round 21 · PR #19863 ·
Fixesdomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T14:11Z. Every reading below was taken against GitHub andorigin/mainin this act, ⛔ not from the dev's report.Triage's measure-first step, answered ⇒ the card stands at p1
Before any edit the dev booted an
ObjectKernelwithObjectQLPluginand a remoteTursoDriverover thelibsqlSQLite double. The only schema door called wassyncSchemasBatch. A declared boolean read back1, JSON read back as a string,paginationTieBreakerwasnull, and the boolean/json/datetime registries were empty. All three halves of the card (read coercion, the managed-object record, the canonical backfill) were real.Review
main; first body lineFixes #19844; no other closing keyword;Clause-②: noat column 0, agreeing with thepatchchangesete4bc63e2dcturso-driver.ts(one private helper,completeRemoteSchemaSync, called by all three remote doors after their DDL), a new test file (12 cases: the three doors read and write alike, keyed byobject, one backfill per batch, a DDL failure registers nothing), a one-clause footnote inturso-remote-deferred-ddl.test.ts, one changesetcompleteRemoteSchemaSync(...)call reds 5 of 12 (the batch-door cases only); restored blob equals HEADdetectManagedDrift) untouched; it is next on this filecheck-governed-merges --pr 19863)successor a roster skip on this head before readyOut-of-scope findings — dispositions:
datestored as a full timestamp; a scalarjsonstored unencoded) are converged by no remote backfill → filed driver-turso remote: cells the pre-#19844 boot door wrote unconverted (a date as a full timestamp, a scalar json unencoded) are converged by no remote backfill, so a stored true reads back as 1 #19868 (verified real by the contract reviewer; the changeset discloses both residues).timevalue9:05(not a recognised time shape) is stored as sent on every door and left alone by the backfill → Acceptance notes (consistent with the changeset's "a cell it cannot safely read as a time").Landing: ready → queue on this head once CI is fully green.
Generated by Claude Code
fix(driver-turso): remote syncSchemasBatch registers read coercion an…
fix(driver-turso): converge remote date/json cells the pre-fix batch …