Skip to content

refactor(driver-sql): move the presumed-timestamp update out of sql-driver.ts into sql-driver-presumed-timestamp.ts (#25521 cut 21) - #27162

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ta8bkd
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-ta8bkd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Part of #25521
Clause-②: no
Metadata-protocol: none
Ruling-ref: 6270097840
Ruling-ref: 6348891160

维护者速读

  • 改了什么:把 sql-driver.ts 里「更新时 updated_at 列是推定存在的,若该表其实没有这一列就每表一次地降级重试」这一个机制(updateWithPresumedTimestamp 与它询问数据库列是否存在的 resolveUpdatedAtColumnPresence 两个方法体)原样搬进新建的 sql-driver-presumed-timestamp.ts;类上的方法、文档注释和签名全部保留,只把方法体转交过去,宿主接口 PresumedTimestampHost 恰好列出方法体读的 7 个成员。
  • 为什么改:[maintainer] refactor(driver-sql): split src/sql-driver.ts (22,096 lines, +2,533 in 30 days) along its SqlDriver seams, one move-only slice per PR #25521 拆分超大文件的第 21 刀;第 20 刀合入后重新测量,这是系列里最后一个收益超过 −30 行的单一机制,sql-driver.ts 减少 34 行。本刀之后余下的每个机制都在 −29 行或以内,或被系列限制钉住,席位可据此收束该系列。
  • 风险与回滚:纯搬移,无行为变化;三个 SQL 驱动的测试两端逐条一致,公开类型声明哈希不变,60 条探针读数两端除一条外一致(两个桩方法不再是 AsyncFunction,返回值仍是同一个 Promise,系列既有形态)。回滚即还原本 PR。
  • 先看:sql-driver-presumed-timestamp.ts(头注释、PresumedTimestampHost、两个函数体);sql-driver.ts 中这两个方法的转交桩;sql-driver.ts 新增的 3 行 import。
  • 两分钟验证:pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-24835-update-leaves-caller-data.test.ts src/sql-driver-11176-bulk-and-merge-updated-at.test.ts,预期全部通过。
  • 需要你做的:无,席位落地。

What changes

Cut 21 of the sql-driver.ts split (#24586 ruling A, recorded in 6270097840). The claim is 6463125783. Cut 20 (#27127) merged first, as 80064b2556; this cut opens with a fresh seam table taken on origin/main 1900c02cb (below) and takes the one remaining area that is a single mechanism, moves cleanly and pays more than -30: the presumed-timestamp update. The bodies of updateWithPresumedTimestamp and resolveUpdatedAtColumnPresence move unedited into a new seam module, packages/drivers/driver-sql/src/sql-driver-presumed-timestamp.ts, through its host PresumedTimestampHost.

updateWithPresumedTimestamp issues an UPDATE whose updated_at stamp is a presumption (a managed object registered without DDL), fenced in a savepoint inside a caller transaction, and, if the write fails, asks the database whether the column is there; an absent answer settles the table, logs one warning and re-issues the caller's payload without the stamp. resolveUpdatedAtColumnPresence is that question: a columnInfo() catalog read on the physical table, null when it cannot be answered. Its only caller is updateWithPresumedTimestamp, so the two are one mechanism. The two callers of the update, update and updateMany, stay in the class and still hand it the issue callback that owns the WHERE and the tenant scope.

Why a new module. No existing seam module holds anything about update stamping; the nearest (sql-driver-autonumber.ts) only shares attemptWithoutPoisoning, which stays on the class. The new module imports sql-driver.ts for a type only (import type { SqlDriver }), plus import type of Knex and DriverOptions, so it adds no evaluation cycle, and the bodies read no module-level declaration, so nothing gains export and nothing else moves.

PresumedTimestampHost lists exactly the seven members the bodies read, each typed as the class member (SqlDriver['NAME']): the knex, logger, physicalTableByObject, physicalSchemaByObject and updatedAtColumnState fields (as getters), and the protected attemptWithoutPoisoning and resolveUpdatedAtColumnPresence methods (as late-bound closures). So updateWithPresumedTimestamp still reaches resolveUpdatedAtColumnPresence through the driver: a subclass override or an instance spy of it is still the one called (probe readings 4 and 5).

The change is move-only, and the card stays open for the seat to close the series. There is no docs/adr/** edit, no packages/spec/** edit, no anchor entry change, no gate edit, no test edit and no new gate. Branch claude/project-thread-ta8bkd (the platform assigned it to this card thread, as the claim records). The move is one commit, 0379b7673, on 1900c02cb.

Net line count (the seat's rule). sql-driver.ts goes from 18,367 lines to 18,333: -34.

  • Out: the two bodies, 57 lines (16 + 41).
  • In: the two stub bodies, 10 lines each, and 3 import lines (a 2-line comment and the ./sql-driver-presumed-timestamp.js import, placed after cut 20's seam import).
  • sql-driver-presumed-timestamp.ts is new: 130 lines (a 42-line header, three type imports, the 16-line host and the two functions with their one-line docblocks).

Path readers. No test, script, doc, ADR or anchor entry reads either body by path, so none is migrated. sql-driver-11176-bulk-and-merge-updated-at.test.ts names updateWithPresumedTimestamp in a comment, still true: the method is still on the class.

Serial with the file's in-flight PRs. I read the file list of every open PR on objectstack-ai/objectstack at the start (27 open, after #27127 merged) and again before opening this one (19 open). None touches packages/drivers/driver-sql/**.

Seam table, fresh, on 1900c02cb

  • Size and churn. sql-driver.ts is 18,367 lines. The 30-day column counts git blame lines from commits on or after 2026-09-28T08:10Z. scripts/pm/git-history.mjs count --ref=origin/main counts 78 first-parent commits on the file in 30 days and 32 in the last 7 (the split cuts are most of them). In all, 3,313 lines (18.0%) are from the last 30 days.
  • Class shape. SqlDriver spans lines 4250-18292 with 350 members: 243 instance methods, 13 accessors and 32 statics, all 32 private or protected.
  • How it was measured. The same TypeScript compiler AST script cuts 9-20 used, which strips casts, parentheses, ! and satisfies before it reads this. Sections are the class's own banner comments. The file is byte-identical to refactor(driver-sql): move date bucketing out of sql-driver.ts into sql-driver-date-bucket.ts (#25521 cut 20) #27127's head 7c6f6f1fe8, where I took a pre-measure while it was in the queue.
Lines Section First … last member Lines 30-day Methods (public) Inferred public getBuilder sites Statics (private/protected)
1-4249 Module preamble - 4,249 701 - - - -
4250-5799 Class head: fields, statics, constructor, capability getters name … assertDialectEmits 1,550 200 8 (1) 1 0 18 (18)
5800-6231 Lifecycle connect … disconnect 432 155 8 (3) 0 0 6 (6)
6232-8638 CRUD — IDataDriver core find … delete 2,407 745 43 (7) 0 12 0 (0)
8639-9168 Bulk & Batch Operations bulkCreate … count 530 70 9 (6) 0 7 0 (0)
9169-9265 Raw Execution rawStatementFault … noteRuntimeIndexDdl 97 28 3 (1) 0 0 0 (0)
9266-9358 Transactions beginTransaction … rollbackTransaction 93 46 7 (5) 0 0 0 (0)
9359-9855 Aggregation aggregate … unresolvableAggregateColumnRefusal 497 54 5 (1) 0 1 0 (0)
9856-10117 Distinct distinct … unresolvableDistinctColumnRefusal 262 26 3 (1) 0 1 0 (0)
10118-10233 Window Functions findWithWindowFunctions … findWithWindowFunctions 116 4 1 (1) 0 1 0 (0)
10234-10332 Query Plan Analysis explain … analyzeQuery 99 1 2 (2) 0 1 0 (0)
10333-10391 Schema Sync (syncSchema / init) syncSchema … reclaimSpace 59 34 3 (3) 0 0 0 (0)
10392-11816 Schema Sync (syncSchema / init) › Data-lifecycle rotation (ADR-0057 P2) rotationStateByTable … migrateMysqlTimeColumns 1,425 219 26 (4) 0 0 0 (0)
11817-11997 Schema Sync (syncSchema / init) › Deferred schema DDL (#3917) setDeferredDdl … flushDeferredSchemaDdl 181 46 6 (4) 0 0 0 (0)
11998-12812 Schema Sync (syncSchema / init) › Managed-schema drift & reconcile (#2186) dialectName … createNullSafeUniqueIndex 815 237 23 (3) 0 0 0 (0)
12813-12864 Schema Introspection listPhysicalColumns … introspectSchema 52 46 2 (2) 0 0 0 (0)
12865-13135 Internal helpers getKnex … auditMissingTenant 271 73 8 (1) 0 1 0 (0)
13136-15185 Internal helpers › Filter helpers tableNameForBuilder … applyFalseConstant 2,050 323 44 (2) 0 0 0 (0)
15186-15361 Internal helpers › Field mapping mapSortField … mapAggregateFunc 176 2 6 (0) 0 0 0 (0)
15362-15409 Internal helpers › Window function builder buildWindowFunction … buildWindowFunction 48 26 1 (0) 0 0 0 (0)
15410-17313 Internal helpers › Column creation helper nowColumnDefault … applyDeclaredColumnDefault 1,904 101 19 (0) 0 0 8 (8)
17314-17611 Internal helpers › Database helpers sqliteFilename … isJsonField 298 76 9 (2) 0 0 0 (0)
17612-18088 Internal helpers › SQLite serialisation formatInput … formatOutput 477 27 2 (0) 0 0 0 (0)
18089-18292 Internal helpers › Introspection internals introspectColumnOrder … introspectUniqueConstraints 204 73 5 (0) 0 0 0 (0)
18293-18367 After the class - 75 0 - - - -

Pricing the areas

Eligibility is as cuts 11-20 defined it: an annotated return type or not public; no getBuilder, this.FIELD.delete/insert/update(, private-static, super, bare-this or this-write site; not insertOnlyUpsertColumns; not a member pinned in place (createColumn, applyFilterCondition, formatInput/formatOutput, initObjects, ensureShardTable, computeTenantField, migrateMysqlDatetimeColumns/migrateMysqlTimeColumns, compileFilters). Each area is priced per mechanism (cut 19's correction), as its stubs minus its bodies, then net of its import edits. The leading row was measured by running the move script on a scratch copy of the file; the rest are the stub estimate (a stub costs 4 lines, +2 per field read, +1 per method read).

Area Bodies Bodies net Declarations and imports Disposition
Presumed-timestamp update (updateWithPresumedTimestamp -31, resolveUpdatedAtColumnPresence -6, measured) 2 -37 (measured: 57 out, 20 in) 0 declarations; +3 for the new module's import and its 2-line comment Moves (this cut). -34 measured. One mechanism: resolveUpdatedAtColumnPresence's only caller is updateWithPresumedTimestamp.
migrateMysqlDatetimeColumns -39, migrateMysqlTimeColumns -32 1 / 1 Pinned: each wraps a vocabulary callee in a catch that logs through logDurabilityFailure, which check:durability-log-level resolves within one file only (cut 12).
createDatabase -35, explainRowSizeOverflow -45, applySqliteJournalMode -46, applyAutoVacuumIncremental -54, the hash-shadow members (-57 to -83), varcharColumnChars -57 Series limit: each reads a private static.
ensureSequencesTable -41 1 Series limit: writes this.
registerManagedObjectMetadata -74 1 Series limit: this.FIELD.delete( sites (tenant-audit census).
Public read/write doors (upsert, aggregate, bulkCreate, findRows, distinct, findWithWindowFunctions, create, analyzeQuery) Series limit: getBuilder-building areas (check:tenant-chokepoint's SCAN_FILES does not cover the seam modules).
Filter helpers remainder (largest presentReadValue -29) Below -30 per mechanism; ⛔ no stitching. compileFilters (-75) is refused (inferred return type).
Everything else -29 or less per mechanism.

Why

Why this area, as one area

  • One mechanism. One question, "issue this UPDATE with a presumed updated_at and recover once if the column is not there", and its one sub-question, "does the physical table carry updated_at". Their callers and every member they read stay in the class.
  • It is the last single mechanism over -30. -34 measured; the next is -29 (presentReadValue).
  • No series limit is touched. Neither body builds through getBuilder: resolveUpdatedAtColumnPresence reads the table's catalog with columnInfo() on Knex directly (the same kind of read check:tenant-chokepoint's header names for listPhysicalColumns, with no rows to scope), and updateWithPresumedTimestamp builds nothing itself (the caller's issue callback builds and scopes the statement, and stays in update / updateMany). Neither body reads a static, has a this.FIELD.delete/insert/update( site, uses super or bare this, or assigns to this (updatedAtColumnState.set(…) mutates the map the getter returns, the same map). Both methods are protected with annotated return types (a Promise of boolean | null, and of number). check:tenant-chokepoint's D8 predicates, measured with its own nullArmsOf: 4 at base and 4 at head, all in sql-driver.ts, 0 in the moved bodies and 0 in the module.
  • Durability gate. resolveUpdatedAtColumnPresence's catch returns null and calls no logDurabilityFailure; neither member appears in check:durability-log-level --list at base. check:durability-log-level --list prints 165 lines at both ends, identical except that two runWideningAlters seams in sql-driver.ts move 34 lines up and logDurabilityFailure's line 3 down (the import).
  • Subclasses and spies. TursoDriver and SqliteWasmDriver override neither method nor a member they read (SqliteWasmDriver assigns this.logger in its constructor; the host reads logger through a getter at call time). The tests that reach these paths (sql-driver-24835-update-leaves-caller-data.test.ts, sql-driver-11176-bulk-and-merge-updated-at.test.ts) call update / updateMany or read updatedAtStampIsPresumed / updatedAtColumnState through (driver as any); all still reach the class.
  • Path readers, screened. No script, test, doc or ADR names either member by a sql-driver.ts#… pointer or a sql-driver.ts::… key, and no scripts/adr-anchors/** record names them or the presumed stamp.

The shape (#23050 shape B, as cuts 1-20 apply it)

  • Each class method keeps its docblock and signature, byte-identical except that the stub drops async (it returns the moved async function's promise, as every earlier async stub in this file does, for example cut 11's autonumber stubs). Its body is now one host literal plus NAME.call(host, …), marked [#25521 cut 21]. The stub cannot throw before the call, so a failure is still a rejected promise (probe readings 4 and 6).
  • The fields go as GETTERS that read the driver at call time, and the methods as closures that call them on the driver at call time. So a subclass override or an instance spy is still the one the body reaches, and a knex or logger replaced after construction is the one read (probe readings 3, 4, 5).
  • Comments. The module header says what moved, what stayed and how the bodies reach the class. The sql-driver.ts import comment names cut 21 in 2 lines. Neither cites an ADR-NNNN letter. The methods' docblocks ([commit 479fba50d] tags, no ADR letter) stay in sql-driver.ts.
  • How the move was made. A script produced it from the source text; nothing was retyped. Each body line is re-indented two columns; no body line starts inside a template literal.

Verification

ADR anchors

  • The count. check:adr-anchors counts 1900 decision-letter citations at base 1900c02cb (a clean detached worktree) and 1900 at the move commit, over 91 anchored files at both ends; the two outputs are byte-identical. Both runs exit 0.
  • After main moved. main moved 11 commits under the cut (to 3346da26df, none in packages/drivers/**). Its tip counts 1900 letters (a clean detached worktree), and so does a local, unpushed merge of the move commit with it; both exit 0. I did not merge main into this branch: git merge-tree --write-tree of the head with it exits 0, so the tree is not stale.
  • Why equal. No comment inside the moved bodies or the new header cites a decision letter, and the module is not anchored.
  • No entry needs a new home. No anchor entry names the moved members. check:adr-symbol-anchors output is byte-identical at both ends.

The published .d.ts does not change

This is measured under the series bar (ruling 6348891160, item 1).

  • How each end was built. pnpm turbo run build --filter=@objectstack/driver-sql --force at each end, against the same dependency tree. The base leg writes base sql-driver.ts into this worktree and removes the new module, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes, an empty git diff HEAD and 0 status entries (RESTORE-OK 89b6a55f6a… 28c6eab163…).
  • The hashes. The raw sha256 of dist/index.d.ts and dist/index.d.mts is the same at base and at the move commit: 0f33bf3febaac3c7aa96f68a4226d752766b8203ab7acade523b69f9aaa44d74 (unchanged since cut 8); cmp exit 0. These two are the dist's only declaration files (no chunk), so the declaration-reorder bar of ruling 6441032442 does not come into play.
  • Positive control. I dropped protected from updateWithPresumedTimestamp in a copy of the head index.d.ts (1 hit, then 0). The copy hashes to 0f3d0fcb2cc407bc147f3506f3ada0018db26ad0a581c40e6a62355bb73222ab, with cmp exit 1.
  • Export lists. CJS require of each dist gives 59 names (sha1 05757ce5ae at both ends); the ESM namespace of index.mjs gives the same 59 at both ends (sha1 05757ce5ae). Neither moved function nor PresumedTimestampHost is among them.
  • The head dist carries the move. .call(host sites in index.mjs go from 70 at base to 72 at head (the two stubs).

Move-only proof

The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base 1900c02cb against the move commit 0379b7673. It is a scratch script and is not committed.

Moved body Body tokens Comments Parameter tokens less this Return type tokens Body lines
resolveUpdatedAtColumnPresence 125 2 9 6 16
updateWithPresumedTimestamp 262 0 38 4 41
  • The moved bodies. 0 tokens differ and every comment is identical. Each body line is the base line less two leading spaces. Both are async at both ends (the module functions carry it).
  • The stubs. Leading trivia (the docblock), modifiers other than async, and the signature are identical to base.
  • The class. It has 350 members at both ends, in the same order, and the 348 unmoved members are byte-identical, trivia included.
  • Top level of sql-driver.ts. 180 of 181 base statements are byte-identical; the other is the class. The head adds one statement, the new import (its 2-line comment is its leading trivia).
  • Result. PROOF OK, exit 0.
  • Negative control. Through scripts/ablation-replace.mjs, I changed if (columns.length === 0) return null; to === 1 in the module (anchor 1 hit, then 0, blob 28c6eab163 to 8b1b9275bb, checked on disk). The proof then reports resolveUpdatedAtColumnPresence as differing (body), prints PROOF FAILED, and exits 1. The restore was proved (blob 28c6eab163 equals HEAD, empty git diff HEAD, 0 status entries).

Tests

Each end ran under one os-verify-lock hold, after building that end's driver-sql dist. I compared the vitest JSON per test name and status, base 1900c02cb against the move commit 0379b7673. Every part's exit code was captured on its own (build, probe, three suites: all 0 at both legs).

Suite Base Head Lost / changed / added
@objectstack/driver-sql 4061 (3847 passed, 214 skipped; 248 files) same 0 / 0 / 0
@objectstack/driver-turso, against that end's built driver-sql dist 2526 (2492 passed, 34 skipped; 95 files) same 0 / 0 / 0
@objectstack/driver-sqlite-wasm 746 (746 passed; 39 files) same 0 / 0 / 0
  • Type check and lint. tsc --noEmit -p . in driver-sql exits 0 on the move commit. pnpm exec eslint --no-inline-config packages/drivers/driver-sql exits 0 (driver-sql has no lint script of its own).
  • Late-binding probe. A scratch script, not committed. It runs node against each end's built dist (real in-memory SQLite tables) and prints 60 readings, each with the result or the error's class, message and top stack frame:
    1. update() on a presumed table without and with a physical updated_at, twice each (the recovery, the warning, the absent / present settling, then the settled path);
    2. update() inside a caller transaction (the savepoint fence; the transaction is still usable afterwards) and updateMany() outside and inside one;
    3. resolveUpdatedAtColumnPresence directly: present, absent, a missing table, a physicalTableByObject alias, a physicalSchemaByObject schema, on a transaction, a throwing columnInfo() and an empty one (knex replaced on the instance), then knex restored;
    4. updateWithPresumedTimestamp directly with fake issue callbacks over every branch (absent / unknown / present answers, with and without a transaction), with an instance spy on resolveUpdatedAtColumnPresence and on attemptWithoutPoisoning (both reached, arguments recorded), the caller's payload left intact, and logger and updatedAtColumnState replaced on the instance (the replacements are the ones read);
    5. SUBCLASS overrides of resolveUpdatedAtColumnPresence (answering false, then null) and of attemptWithoutPoisoning, reached from update();
    6. a non-column failure rethrown unchanged, and a PostgreSQL driver whose server is unreachable (null);
    7. the two methods' shape on the prototype (type, arity, constructor name).
  • Probe result. 59 of the 60 readings are identical. The other is reading 7: the two stubs' constructor name is Function at head where it was AsyncFunction at base (type function and arity 2 / 4 identical). That is the stub dropping async, as every earlier async stub of this series does; no test, consumer or .d.ts reads it.
  • Not run. No ablation, because no tested behaviour changed.
  • test:repo. Not run locally; it is CI's run on this PR.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 48 families for this diff (the same 48 cuts 17-20 derived). I ran each one on its own and captured its exit code before any pipe, after a full pnpm build of the move commit (116 of 116 tasks).

  • Move commit 0379b7673: 48 run, all 48 exit 0. --ran reconciles: 48 derived famil(ies) accounted for — 48 run, 0 NOT-MEASURED (a DERIVED zero — all 48 recorded an exit code and none of them is 3), exit 0. (I fetched the pinned fixture commit of check-plugin-teardown-shape.mjs --self-test into this shallow clone first.)
  • Hand gates, all exit 0: check-adr-anchors, check-adr-symbol-anchors, check-tenant-audit-census, check-platform-checklist --changed-from origin/main, check-commit-card-trailers --range origin/main..HEAD, eslint --no-inline-config packages/drivers/driver-sql, check-durability-degradation-log-level --list. In a clean detached worktree at base 1900c02cb and on the move commit, check-adr-anchors, check-adr-symbol-anchors and check-tenant-audit-census print byte-identical output; check-object-def-param-keys differs only in its file count (3392 to 3393, the new module); check-durability-degradation-log-level --list prints the same 165 lines, line numbers aside.
  • No gate silently empty. Each derived gate's log prints a non-zero population (for example check:object-def-param-keys compares 7 override parameter positions over 3393 files with every control firing; check:tenant-chokepoint reads 24 getBuilder() bindings across 3 files; check:durability-log-level reads 48 durability-critical catch seams and 89 read seams; check:dual-build-cjs-loads loads 112 entry points across 69 packages; check:driver-conformance covers 55 cells, 0 in its DEBT ledger).

The 48 derived commands: node scripts/check-closing-keyword-parity.mjs, node scripts/check-closing-keyword-parity.mjs --self-test, node scripts/check-comment-mask-adoption.mjs, node scripts/check-comment-mask-adoption.mjs --self-test, node scripts/check-comment-mask-corpus.mjs, node scripts/check-dts-emitted.mjs --self-test, node scripts/check-issue-citations.mjs, node scripts/check-plugin-teardown-shape.mjs, node scripts/check-plugin-teardown-shape.mjs --self-test, node scripts/ci/assert-turbo-remote-cache.mjs --self-test, node scripts/ci/plan-test-core-shards.mjs --self-test, node scripts/ci/prune-turbo-cache.mjs --self-test, node scripts/docs-audit/check-affected-docs.mjs, node scripts/docs-audit/check-drift-comment.mjs, pnpm --filter @objectstack/spec run check:duration-unit-keys, pnpm check:ci-filter-parity, pnpm check:closed-ceremony-text, pnpm check:cross-package-test-inputs, pnpm check:dispatcher-error-vocabulary, pnpm check:doc-authoring, pnpm check:driver-conformance, pnpm check:driver-memory-census, pnpm check:dts-closure, pnpm check:dual-build-cjs-loads, pnpm check:durability-log-level, pnpm check:error-status-conformance, pnpm check:gitlink-declared, pnpm check:issue-citations, pnpm check:keyed-text-bounds, pnpm check:lean-entry-closure, pnpm check:logger-receiver-detach, pnpm check:nul-bytes, pnpm check:object-def-param-keys, pnpm check:org-identifier, pnpm check:page-declaration-shape, pnpm check:platform-object-tenancy-census, pnpm check:published-files, pnpm check:refd-timer-probe, pnpm check:registry-log-declared, pnpm check:rest-log-spy-declared, pnpm check:slot-lookup, pnpm check:sourcemap-no-sources-content, pnpm check:system-context-census, pnpm check:tenant-chokepoint, pnpm check:test-source-alias, pnpm check:tier-file-adoption, pnpm check:undeclared-dep-imports, pnpm check:watch-hint-literal.

Acceptance notes

  • check:tenant-chokepoint's SCAN_FILES does not list sql-driver-presumed-timestamp.ts. The moved bodies hold 0 tenant predicates and build through no getBuilder (one columnInfo() catalog read, and the caller's issue callback). Noted, not filed: widening SCAN_FILES would be a gate change.
  • Stale links. sql-driver.ts has the same 30 {@link X} sites (21 names) that are neither a top-level binding nor a SqlDriver member at base and at head (the name set is identical). The module has none.
  • The series' remaining yield, priced on this base. After this cut, no single mechanism in sql-driver.ts pays more than -30 under the net-line rule and the series limits: the largest free one is presentReadValue at -29, and every larger member is pinned (createColumn, applyFilterCondition, formatInput/formatOutput, initObjects, ensureShardTable, compileFilters, the two MySQL widenings) or blocked by a series limit (getBuilder-building doors, private-static readers, this writers, this.FIELD.delete( sites). The seat can close the series on this cut.

Human Minimum

  • 1. No packages/spec change.
  • 2. One new name, PresumedTimestampHost, in a new module that index.ts does not re-export.
  • 3. No formatter ran; scoped eslint --no-inline-config passes (repo-wide pnpm lint is CI's run).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. No publishable behaviour change: skip-changeset; both declaration lines above.
  • 6. packages/spec not touched.
  • 7. No hand edits to generated docs, changelogs or release notes.
  • 8. Title, body and comments are in English (the maintainer brief excepted).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5oBtKR8pYepVfbCoGV

…river.ts into sql-driver-presumed-timestamp.ts

The bodies of updateWithPresumedTimestamp (the UPDATE whose updated_at stamp
is a presumption, recovering once per table when the column is not there)
and resolveUpdatedAtColumnPresence (the columnInfo() question that recovery
asks) move unedited into a new seam module. Both methods stay on the class
with their docblocks and signatures, and hand the bodies exactly the members
they read through PresumedTimestampHost: knex, logger, physicalTableByObject,
physicalSchemaByObject and updatedAtColumnState as getters,
attemptWithoutPoisoning and resolveUpdatedAtColumnPresence as late-bound
closures. Move-only: no behaviour change.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5oBtKR8pYepVfbCoGV
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 13 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/wire-format.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/automation/webhooks.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/data-modeling/queries.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/data-modeling/seed-data.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/deployment/cli.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/deployment/seed-tenancy-repair.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/permissions/system-context.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/protocol/kernel/http-protocol.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/protocol/objectql/schema.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/protocol/objectql/security.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/protocol/objectql/state-machine.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/ui/views.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/releases/v16.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/releases/v17/17-3.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/releases/v17/17-5.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/releases/v17/17-6.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))
  • content/docs/releases/v17/17-7.mdx (via updated_at (literal, a string literal in resolveUpdatedAtColumnPresence; a string literal in updateWithPresumedTimestamp))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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

Which tree this was computed on

This run read content/docs from 881e3aea52cc0c26d5499764dcfde3a11d35b755 — the merge of head 0379b7673a7d87b745e0aafd6dcfcba8a3a33650 into base b15b2fcd028a33c6e54ea5aba373ebb884e82957, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 881e3aea52cc0c26d5499764dcfde3a11d35b755 && git checkout 881e3aea52cc0c26d5499764dcfde3a11d35b755
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b15b2fcd028a33c6e54ea5aba373ebb884e82957 0379b7673a7d87b745e0aafd6dcfcba8a3a33650 && git checkout -B drift-repro b15b2fcd028a33c6e54ea5aba373ebb884e82957 && git merge --no-ff 0379b7673a7d87b745e0aafd6dcfcba8a3a33650

node scripts/docs-audit/affected-docs.mjs --json b15b2fcd028a33c6e54ea5aba373ebb884e82957

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b15b2fcd028a33c6e54ea5aba373ebb884e82957 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-28T09:17Z · head 0379b7673 · #25521 cut 21

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main, assignee os-project-manager, labelled skip-changeset because this is a move.
    • The body opens with Part of #25521 (not Fixes). Clause-②: no and Metadata-protocol: none are line-initial, with Ruling-ref: 6270097840 and 6348891160.
    • check-governed-merges --pr 27162 reads NOT governed, 212 changed lines.
    • The claim (6463125783) names this PR's branch.
  2. Area against the order: cut 20 priced the presumed-timestamp update at about −34. The dev took it as one mechanism: updateWithPresumedTimestamp and its only callee resolveUpdatedAtColumnPresence move into the new sql-driver-presumed-timestamp.ts through PresumedTimestampHost. sql-driver.ts goes from 18,367 to 18,333 lines (net −34), which clears the −30 bar.
  3. Move check (the seat's own): every non-blank line removed from sql-driver.ts against the merge base was compared with the lines added, indentation stripped.
    • Every body line is present in the new module.
    • The only two removed-not-added lines are the stub signatures, which drop async (disclosed; the series' existing stub shape). Each stub returns the module function's promise and does nothing that can throw before it, so a caller sees the same promise.
    • The host keeps attemptWithoutPoisoning and resolveUpdatedAtColumnPresence as late-bound closures, so a subclass override or an instance spy is still the one reached. knex, logger, the two physical maps and updatedAtColumnState are getters.
    • The module imports sql-driver.ts for a type only, so there is no evaluation cycle.
  4. Published .d.ts and ADR letters: the raw sha256 is 0f33bf3f… at base and at head, and the positive control changes it. check:adr-anchors reads 1,900 at base, head, the main tip and a local merge with it. The AST proof is OK, and its negative control fails.
  5. Behaviour: the three driver suites match base per test, with 0 lost, changed or added (driver-sql 4,061, driver-turso 2,526, driver-sqlite-wasm 746).
  6. Gates:
    • Locally 48 of 48 derived families plus 7 hand gates exit 0, with 0 NOT-MEASURED.
    • CI on head 0379b7673: 34 success, 18 skipped, 0 red.
  7. Overlap: git merge-tree reports no conflict against main 6866e2e7. No other open PR touches packages/drivers/driver-sql/**.
  8. Series end: after this cut, no free mechanism in sql-driver.ts pays more than −30. The largest is presentReadValue at −29; every larger member is pinned or series-limited. The seat closes the [maintainer] refactor(driver-sql): split src/sql-driver.ts (22,096 lines, +2,533 in 30 days) along its SqlDriver seams, one move-only slice per PR #25521 cut series on this landing. The dev's other note (check:tenant-chokepoint SCAN_FILES does not list the new module, which holds 0 tenant predicates) is a gate change and is not filed.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 09:18
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 09:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit edb72d9 Oct 28, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ta8bkd branch October 28, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants