Skip to content

refactor(driver-sql): move the upsert conflict-target path out of sql-driver.ts into sql-driver-upsert-conflict-target.ts (#25521 cut 14) - #26408

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-h28rw8
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-h28rw8

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 里 upsert 的「冲突键预检」代码(判断表上是否真有能承接 conflictKeys 的唯一索引、MySQL 下是否会落到别的唯一键上,以及多组织写入的组织栅栏两个小助手)原样搬进新文件 sql-driver-upsert-conflict-target.ts;类上的方法全部保留,只把方法体转交过去。
  • 为什么改:[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 拆分超大文件的第 14 刀;这块在剩余可搬区域里收益最大,sql-driver.ts 减少 228 行。
  • 风险与回滚:纯搬移,无行为变化;三个 SQL 驱动的测试两端逐条一致,公开类型声明哈希不变。回滚即还原本 PR。
  • 先看:sql-driver-upsert-conflict-target.ts 的头注释与 host 接口;sql-driver.ts 中五个方法的转交桩;refuseUnbackedConflictTarget 上方那段文档注释如今重新紧贴它自己的函数。
  • 两分钟验证:pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-upsert-conflict-target-dialects.test.ts src/sql-driver-upsert-conflict-target-envelope.test.ts,预期全部通过。
  • 需要你做的:无,席位落地。

What changes

Cut 14 of the sql-driver.ts split (#24586 ruling A, recorded in 6270097840). The claim is 6431182919. The cut opens with a fresh seam table (below), taken on origin/main abb904605 (cut 13's merge), and takes the area that pays best without breaking a series limit: the conflict-target path of upsert. It moves unedited into a new seam module, packages/drivers/driver-sql/src/sql-driver-upsert-conflict-target.ts, through a new host UpsertConflictTargetHost:

The change is move-only, and the card stays open for later cuts. There is no docs/adr/** edit, no packages/spec/** edit, no anchor entry change, no test change, no gate edit and no new gate. Branch claude/project-thread-h28rw8 (the platform assigned it to this card thread, as the claim records). The move is one commit, 84d0e45bd, on abb904605.

Net line count (the seat's rule). sql-driver.ts goes from 20,022 lines to 19,794: -228.

  • Out: the five bodies, 134 lines (9 + 4 + 17 + 97 + 7). Three module-level declarations that only these bodies read, 125 lines with their own docblocks plus 3 separator lines: indexCoversConflictTarget (11), unbackedConflictTargetCause (11) and refuseAmbiguousConflictTarget (103).
  • In: the five stub bodies, 30 lines (6 + 2 + 7 + 8 + 7), and a 4-line import of the module with its comment.
  • The new module is 359 lines.

Why -228 and not the -320 cut 13 priced. Two corrections, both measured on this base:

  • indexCoversConflictTarget's leading trivia holds two earlier comments that are not its own: the [#8445 → #8567] note on isUnbackedConflictTargetError and the [#8445] docblock of refuseUnbackedConflictTarget (80 lines; the fix(driver-sql): upsert with nothing left to merge is a plain INSERT, not merge-ALL #26015 insertion of indexCoversConflictTarget and unbackedConflictTargetCause had separated that docblock from its function). They describe code that stays, so they stay. With the two functions gone, the [#8445] docblock again sits directly on refuseUnbackedConflictTarget.
  • ConflictTargetVerdict (15 lines) is the declared return type of the class method assertConflictTargetHonoured, whose signature stays in the class (and in the published .d.ts). It stays, exported in place.

Serial with the file's in-flight PRs. I read the full file list of every open PR at start (23 PRs, every page of each). Before cut 13 merged only #26377 touched sql-driver.ts. No other open PR touches sql-driver.ts, turso-driver.ts, sqlite-wasm-driver.ts, check-tenant-chokepoint.mjs or the driver-sql anchor entries. #26138 touches driver-sql/src/builtin-column-collision.ts only; #26337 adds driver-sql/src/sql-driver-external-unprovisioned-sort-anchor.test.ts only.

Seam table, fresh, on abb904605

  • Size and churn. sql-driver.ts is 20,022 lines. The 30-day column counts git blame lines from commits on or after 2026-09-27T01:27:21Z, 30 days before the base commit. History reaches 2026-09-20. scripts/pm/git-history.mjs count proves 74 commits on the file in 30 days and 32 in the last 7. In all, 3,490 lines (17.4%) are from the last 30 days.
  • Class shape. SqlDriver spans lines 5273-19947 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-13 used, which strips casts, parentheses, ! and satisfies before it reads this. Sections are the class's own banner comments.
Lines Section First … last member Lines 30-day Methods (public) Inferred public getBuilder sites Statics (private/protected)
1-5272 Module preamble - 5,272 811 - - - -
5273-6864 Class head: fields, statics, constructor, capability getters name … assertDialectEmits 1,592 290 8 (1) 1 0 18 (18)
6865-7296 Lifecycle connect … disconnect 432 155 8 (3) 0 0 6 (6)
7297-9807 CRUD — IDataDriver core find … delete 2,511 736 43 (7) 0 12 0 (0)
9808-10419 Bulk & Batch Operations bulkCreate … count 612 119 9 (6) 0 7 0 (0)
10420-10516 Raw Execution rawStatementFault … noteRuntimeIndexDdl 97 28 3 (1) 0 0 0 (0)
10517-10609 Transactions beginTransaction … rollbackTransaction 93 46 7 (5) 0 0 0 (0)
10610-11141 Aggregation aggregate … unresolvableAggregateColumnRefusal 532 60 5 (1) 0 1 0 (0)
11142-11429 Distinct distinct … unresolvableDistinctColumnRefusal 288 17 3 (1) 0 1 0 (0)
11430-11545 Window Functions findWithWindowFunctions … findWithWindowFunctions 116 4 1 (1) 0 1 0 (0)
11546-11644 Query Plan Analysis explain … analyzeQuery 99 1 2 (2) 0 1 0 (0)
11645-11703 Schema Sync (syncSchema / init) syncSchema … reclaimSpace 59 34 3 (3) 0 0 0 (0)
11704-13252 Schema Sync (syncSchema / init) › Data-lifecycle rotation (ADR-0057 P2) rotationStateByTable … migrateMysqlTimeColumns 1,549 158 26 (4) 0 0 0 (0)
13253-13509 Schema Sync (syncSchema / init) › Deferred schema DDL (#3917) setDeferredDdl … flushDeferredSchemaDdl 257 25 6 (4) 0 0 0 (0)
13510-14324 Schema Sync (syncSchema / init) › Managed-schema drift & reconcile (#2186) dialectName … createNullSafeUniqueIndex 815 237 23 (3) 0 0 0 (0)
14325-14376 Schema Introspection listPhysicalColumns … introspectSchema 52 46 2 (2) 0 0 0 (0)
14377-14647 Internal helpers getKnex … auditMissingTenant 271 73 8 (1) 0 1 0 (0)
14648-16797 Internal helpers › Filter helpers tableNameForBuilder … applyFalseConstant 2,150 354 44 (2) 0 0 0 (0)
16798-16973 Internal helpers › Field mapping mapSortField … mapAggregateFunc 176 2 6 (0) 0 0 0 (0)
16974-17021 Internal helpers › Window function builder buildWindowFunction … buildWindowFunction 48 26 1 (0) 0 0 0 (0)
17022-18968 Internal helpers › Column creation helper nowColumnDefault … applyDeclaredColumnDefault 1,947 92 19 (0) 0 0 8 (8)
18969-19266 Internal helpers › Database helpers sqliteFilename … isJsonField 298 76 9 (2) 0 0 0 (0)
19267-19743 Internal helpers › SQLite serialisation formatInput … formatOutput 477 27 2 (0) 0 0 0 (0)
19744-19947 Internal helpers › Introspection internals introspectColumnOrder … introspectUniqueConstraints 204 73 5 (0) 0 0 0 (0)
19948-20022 After the class - 75 0 - - - -

Pricing the areas

Eligibility is as cuts 11-13 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. 129 members are eligible on this base. Each candidate area is priced as its stubs minus its bodies, then net of the module-level declarations it shares with members that stay (they stay in place, exported; ⛔ no second copy of a helper), counting only a declaration's own docblock.

Area (section) Bodies Bodies net Declarations that move along Disposition
CRUD upsert conflict-target path (assertConflictTargetHonoured, assertConflictTargetBackedWithoutClause, introspectKeyIndexes, upsertTenantGuard, upsertConflictRefusal) 5 -104 -128 (indexCoversConflictTarget, unbackedConflictTargetCause, refuseAmbiguousConflictTarget), +4 for the module import Moves (this cut). -228 measured.
Backend statement fault (backendStatementFault) 1 -75 about -145 (backendStatementFaultError with its docblock) Not taken: about -216, less, and a single body. Next candidate.
Rotation sub-banner (ensureRotation, registerExternalObject, warnBuiltinColumnCollisions, …) 7 -195 gross 0 Not taken: migrateMysqlDatetimeColumns and migrateMysqlTimeColumns (-71) stay under the cut 12 durability-gate trap, leaving about -124.
The rest of the Filter helpers (compileFilters -75, applyNormalizedComparison -60, …) - - filterArrayReachedDriverError (-44) Not priced as one area: small bodies spread around applyFilterCondition, which an ADR-0139 pointer pins in place.

Why this area

  • It pays the most of any area that moves cleanly. The five bodies are one mechanism: what upsert asks of the table's physical keys before it compiles, and the fence it applies afterwards.
  • What stays, and why.
    • upsert builds through getBuilder (2 sites) and stays, with assertMergeLandedInWrittenOrganization, assertMergeLandedOnSuppliedIdentity and mergeLandsInWrittenOrganization.
    • insertOnlyUpsertColumns stays (drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740).
    • refuseUnbackedConflictTarget (also thrown by upsert's reactive catch), refuseUpsertConflictOutsideWrittenOrganization (also thrown by upsert and assertMergeLandedInWrittenOrganization) and ConflictTargetVerdict (the class method's return type) are shared. Each gains a leading export and nothing else, and the module imports them back. The module reads the two functions only inside function bodies, so the import cycle is evaluation-safe. index.ts names its exports one by one; the export lists below prove none of the three leaks.
    • rotationWriteTarget, introspectIndexes, resolveTenantField and remoteColumn are read by members that stay too. The moved bodies reach them through the host.
  • No series limit is touched. None of the five builds through getBuilder, reads a static, has a this.FIELD.delete/insert/update( site, uses super or a bare this, or writes to this (introspectKeyIndexes calls .get/.set on the physicalKeyIndexes map it reads through a getter, as before). Every one has an annotated return type; three are protected and two private. 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 new module. upsertTenantGuard reads the tenant field to build the fence descriptor and holds no predicate; disclosed because the new module is outside the gate's SCAN_FILES.
  • Durability gate. introspectKeyIndexes has the one catch in the moved code; it returns null and reaches no vocabulary callee. check:durability-log-level reads 48 seams (7 propagating) at both ends.
  • Subclasses. TursoDriver calls upsertTenantGuard and upsertConflictRefusal on its own instance; both stubs keep their signatures and stay protected. TursoDriver and SqliteWasmDriver override none of the five. Every member the bodies call is a late-bound closure on the instance.
  • Path readers, screened. No script, test, doc or ADR names any of the five, or the three moved declarations, by a sql-driver.ts#… pointer or a sql-driver.ts::… key. Prose mentions (live-dialect-matrix.isolation.test.ts, sql-driver-introspection.ts, sql-driver-upsert-conflict-target-envelope.test.ts, the turso parity tests, the driver-sql CHANGELOG) name the functions or sql-driver.ts as a file, and each is still true. No test reads these bodies' text by path, so no test changes.

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

  • Each class method keeps its docblock and signature, byte-identical, except that the three async methods drop async (each stub returns the moved function's promise; the .d.ts does not carry async). Its body is now one host literal plus NAME.call(host, ...), marked [#25521 cut 14]. upsertConflictRefusal reads no member, so its stub calls the function directly (the cut 11 shape).
  • Fields go as GETTERS that read the driver at call time (physicalTableByObject, physicalKeyIndexes). Methods go as closures that call the method on the driver at call time (introspectIndexes, introspectKeyIndexes, remoteColumn, resolveTenantField, rotationWriteTarget). So a subclass override, an instance spy, or a field replaced after construction is still the one the body reaches (probe below).
  • UpsertConflictTargetHost lists exactly those seven members, each typed as the class member's indexed access. There is no cast, and no member becomes public.
  • Module imports. StandardErrorCode (@objectstack/spec/api, read by the moved refuseAmbiguousConflictTarget), type DriverOptions (@objectstack/spec/data), type PhysicalIndex (./schema-drift.js), and from ./sql-driver.js the three shared declarations plus type SqlDriver. No sql-driver.ts import lost its last reader.
  • Comments. The module header says what moved, what stayed and why. The sql-driver.ts import comment names cut 14 in 3 lines. The header cites no ADR-NNNN Dn.
  • 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.

ADR anchors

  • The count. check:adr-anchors counts 1877 decision-letter citations at base abb904605 (a clean detached worktree) and 1877 at head, over 91 anchored files at both ends. Both runs exit 0.
  • Why equal. No comment inside the moved bodies or declarations cites a decision letter. The one ADR-0120 D1 near this code is in upsertTenantGuard's docblock, which stays on the class method.
  • No entry needs a new home. The sql-driver.ts entry's one paragraph (ADR-0053) describes formatOutput and presentReadValue, neither of which moved. No scripts/adr-anchors/** file changed.

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 deletes the new module, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes (25041557, bcb95569), an empty git diff HEAD and 0 status entries.
  • The hashes. The raw sha256 of dist/index.d.ts and dist/index.d.mts is the same at base and at head, 0f33bf3febaac3c7aa96f68a4226d752766b8203ab7acade523b69f9aaa44d74 (unchanged since cut 8).
  • Positive control. I dropped protected from assertConflictTargetHonoured in the head index.d.ts. The result hashes to 0f7f0de877d0becf5acec215a618fa3922518b60690f91898bec355659e1e21a, with a 1-line diff and cmp exit 1.
  • Export lists. CJS require of the head dist gives 59 names, the same set as its ESM import (sha1 05757ce5ae); the ESM export statement of the base and head index.mjs lists the same 59 names (sha1 05757ce5ae both). None of the three newly exported declarations, and nothing from the new module, is among them.
  • The head dist carries the move. .call(host sites in index.mjs go from 47 at base to 51 at head (four stubs; the fifth calls directly).

Move-only proof

The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base abb904605 against head 84d0e45bd. It is a scratch script and is not committed.

Moved body Body tokens Comments Parameter tokens less this Return type tokens Body lines
upsertTenantGuard 89 2 15 15 9
upsertConflictRefusal 23 0 9 1 4
introspectKeyIndexes 111 2 15 8 17
assertConflictTargetHonoured 423 54 11 4 97
assertConflictTargetBackedWithoutClause 114 0 11 4 7
  • Each moved body. In every row, 0 tokens differ and every comment is identical. Each body line is the base line less two leading spaces. async matches the base method on all five functions.
  • The five stubs. Leading trivia (the docblock), modifiers and the signature are identical to base, less async on the three async methods.
  • The class. It has 350 members at both ends, in the same order, and the 345 unmoved members are byte-identical, trivia included.
  • Module-level declarations. The three that moved are byte-identical in the module from their own docblock on, and gone from sql-driver.ts. The three shared ones differ from base by a leading export and nothing else. refuseUnbackedConflictTarget's leading trivia is now exactly the 80 lines of earlier comments that stayed from indexCoversConflictTarget's trivia (checked byte for byte). At sql-driver.ts's top level, 194 of 201 base statements are byte-identical. The rest are the three moved declarations, the three exports and the class; the one head-only statement besides those is the new import.
  • Result. PROOF OK, exit 0.
  • Negative control. Through scripts/ablation-replace.mjs, I changed introspectKeyIndexes(tableName, { fresh: true }) to { fresh: false } in the module (1 hit, checked on disk). The proof then reports assertConflictTargetHonoured with its body tokens differing, prints PROOF FAILED, and exits 1. The restore was proved (blob equals HEAD, empty git diff HEAD).

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 abb904605 against head 84d0e45bd. Every part's exit code was captured on its own (build, probe, three suites: all 0 at both ends).

Suite Base Head Lost / changed / added
@objectstack/driver-sql 4060 (3846 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. pnpm exec eslint --no-inline-config packages/drivers/driver-sql exits 0.
  • Late-binding probe. A scratch script, not committed. It runs node against each end's built dist on in-memory SQLite and prints 50 readings:
    1. upsertTenantGuard on six rows (no options, empty tenantId, the tenant row, another organization's row, a row without the value, a non-tenant object) and upsertConflictRefusal on one, two, empty and absent keys, with instance spies on all five host methods showing the calls in order;
    2. introspectKeyIndexes cold, cached, fresh, on a keyless table and a missing one, with introspectIndexes replaced on the instance by a throwing read (null), and with the physicalKeyIndexes map replaced on the instance after construction (the new map is the one read);
    3. assertConflictTargetHonoured on every verdict: ambiguous named and unnamed, identity at risk (id with rivals) on two tables, honoured, unbacked named and unnamed, a composite unbacked target, a missing table, and a stale cache re-read before a refusal;
    4. assertConflictTargetBackedWithoutClause backed, unbacked, on id, and on a missing table;
    5. a SUBCLASS overriding rotationWriteTarget, resolveTenantField, remoteColumn and introspectIndexes, each reached, and physicalTableByObject shadowed on the instance after construction (the refusal's cause names the shadowed table);
    6. nine upsert()/find() calls end to end on SQLite: a seed and a merge, two unbacked refusals through the reactive catch (one tenant-scoped), and tenant-scoped composite upserts in two organizations, read back;
    7. bad arguments on four of the methods.
  • Probe result. 49 of the 50 readings are identical, refusal code, status, message and cause included. The other one is the bad-argument row of upsertTenantGuard: it throws the same TypeError with the same message, and only the top stack frame's receiver name differs, _SqlDriver.upsertTenantGuard at base and Object.upsertTenantGuard at head. That is the host-literal receiver of shape B, the same at every earlier cut, and no test or consumer reads it.
  • Not run. No ablation, because no tested behaviour changed.
  • test:repo. The repo projects of types (11), core (83), objectql (5), spec (1279) and rest (280 passed, 18 skipped) pass locally after building rest's closure. Runtime's is NOT MEASURED locally; CI's Test Core runs all of them.
  • After the merge of origin/main (8adeb4ba47, 11 commits, no packages/drivers/** file): tsc exits 0, the two upsert conflict-target test files pass (22 passed, 3 skipped), and check:adr-anchors still reads 1877.

Gates

  • Derived list. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 53 commands for the 2 changed paths (merge base abb904605). On 84d0e45bd, 52 exit 0. check:dual-build-cjs-loads exits 3, PREREQUISITE NOT MET (no full build here): NOT MEASURED locally, CI runs it. The teardown self-test's pinned commit was fetched first, and objectql was built for check:lean-entry-closure. After the merge of origin/main the list gains one family, node scripts/ci/plan-test-core-shards.mjs --self-test, which exits 0 on 8adeb4ba47.
  • Reconciliation. dispatch-gates --ran: 54 derived families accounted for, 53 run, 1 NOT-MEASURED (derived from the recorded exit 3), exit 0.
  • Card-named gates, read at base (a clean detached worktree at abb904605) and at head, all exit 0:
Gate Base Head
check:adr-anchors 91 files, 1877 decision-letter citations 91 files, 1877
check:adr-symbol-anchors 2664 resolve 2664 resolve
check:tenant-chokepoint 24 bindings / 3 files, D8 clean; 4 tenant predicates same; 4 tenant predicates
check:durability-log-level 48 seams, all loud (7 propagating) 48 seams, all loud (7 propagating)
check:driver-conformance 55 covered / 0 DEBT same
check:keyed-text-bounds 112 files / 238 index entries same
check:object-def-param-keys 3321 files / 544 classes / 7 positions 3322 files (the new module) / 544 / 7
check:system-context-census 153 sites / 132 symbols same
check-tenant-audit-census 244 sites same
check:logger-receiver-detach 0 detaches, 3762 files 0 detaches, 3763 files (the new module)
  • check:nul-bytes passes. The pre-push check:commit-card-trailers passed on push.

Acceptance notes

  • check:tenant-chokepoint's D8 rule does not scan the new module. The moved bodies hold 0 of the file's 4 tenant predicates (measured with the gate's own nullArmsOf), and none builds a query. Noted, not filed: widening SCAN_FILES would be a gate change.
  • A docblock returns to its function. The [#8445] docblock of refuseUnbackedConflictTarget had been separated from it by the two helpers fix(driver-sql): upsert with nothing left to merge is a plain INSERT, not merge-ALL #26015 inserted; with them moved it again sits on its function. Its bytes are unchanged, and the published .d.ts does not carry it (the function is not exported from index.ts).
  • Next slice, priced on this base. The backend statement fault classifier (backendStatementFault with backendStatementFaultError) pays about -216; it reads logger, so it needs a check:logger-receiver-detach and check:durability-log-level screen first.

Human Minimum

  • 1. No packages/spec change.
  • 2. The one new name, UpsertConflictTargetHost, follows the interface casing.
  • 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_01Cyh2bm4otUm5Vnb5Zw2KDT

…-driver.ts into sql-driver-upsert-conflict-target.ts (cut 14)

Move-only. assertConflictTargetHonoured, assertConflictTargetBackedWithoutClause,
introspectKeyIndexes, upsertTenantGuard and upsertConflictRefusal move their
bodies into the new seam module behind UpsertConflictTargetHost; the class keeps
every method as a stub with its docblock and signature. indexCoversConflictTarget,
unbackedConflictTargetCause and refuseAmbiguousConflictTarget move with them;
refuseUnbackedConflictTarget, refuseUpsertConflictOutsideWrittenOrganization and
ConflictTargetVerdict gain export in place.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/external-datasources.mdx (via remoteColumn (symbol, a field of interface UpsertConflictTargetHost), remoteColumn (literal, a string literal in remoteColumn; a string literal in upsertTenantGuard))
What this run could not see
  • the SDK route bridge reached 55 of 207 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 c51aed4f9d9bc346274c03bc2ac1aaafbf2e8d7a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 39ee0fae4e1e6aa77f901ada4d3123f5f8501a3d — the merge of head 8adeb4ba47b2c8ab7a983edbd6139e242110666f into base c51aed4f9d9bc346274c03bc2ac1aaafbf2e8d7a, 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 39ee0fae4e1e6aa77f901ada4d3123f5f8501a3d && git checkout 39ee0fae4e1e6aa77f901ada4d3123f5f8501a3d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c51aed4f9d9bc346274c03bc2ac1aaafbf2e8d7a 8adeb4ba47b2c8ab7a983edbd6139e242110666f && git checkout -B drift-repro c51aed4f9d9bc346274c03bc2ac1aaafbf2e8d7a && git merge --no-ff 8adeb4ba47b2c8ab7a983edbd6139e242110666f

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

⚠️ 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 c51aed4f9d9bc346274c03bc2ac1aaafbf2e8d7a → 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-27T03:08Z · head 8adeb4ba47 · #25521 cut 14

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

  1. Shape: a draft against main. The body opens with Part of #25521, not Fixes, because the card stays open for later cuts. Clause-②: no and Metadata-protocol: none are line-initial. skip-changeset applies because this is a move. check-governed-merges --pr 26408 reads NOT governed, 665 changed lines. None of the four contract faces is touched, so no at-tier review is owed.

  2. Premise: the claim 6431182919 priced the CRUD upsert conflict-target path at about −320. It lands at −228 (20,022 → 19,794). The dev measured why it falls short:

    • About 80 lines of leading comment belong to code that stays. One is the [#8445] docblock of refuseUnbackedConflictTarget, which now sits on its function again.
    • ConflictTargetVerdict is the return type of a class method, so it stays and gains export.

    Five bodies move into sql-driver-upsert-conflict-target.ts behind UpsertConflictTargetHost (7 members): assertConflictTargetHonoured, assertConflictTargetBackedWithoutClause, introspectKeyIndexes, upsertTenantGuard and upsertConflictRefusal. The last is a direct call, because it reads no member. Each method stays on the class as a delegate.

  3. Move check (the seat's own): every line removed from sql-driver.ts was compared with the new module after stripping indentation. All 254 non-blank lines are present there except six:

    • Three method signatures that lose async because they now return the moved body's promise.
    • Three declarations that code staying in the class still reads, each gaining export in place (cut 2's shape): refuseUnbackedConflictTarget, refuseUpsertConflictOutsideWrittenOrganization and ConflictTargetVerdict.

    Three helpers read only by the moved bodies move with them, byte-identical. index.ts re-exports sql-driver.js by name only, and none of the newly exported names is among those names. The dev reports the public .d.ts hash 0f33bf3f unchanged, with a positive control.

  4. Readings at base and head (the dev's):

    • check:adr-anchors: 1,877 = 1,877.
    • Tenant-chokepoint D8 predicates: 4 = 4, with 0 in the new module.
    • Durability seams: 48 = 48.
    • The three driver suites are identical.
  5. Gates: at this stamp every check-run on the head reads success or skipped (path-filtered UI, docs build, the dogfood matrix placeholder, opt-in harnesses and the skipped changeset check). None reads failed or in progress, and Test Core 1–8, Lint & Repo Gates and TypeScript Type Check are green. Locally the dev reports 53 of 54 derived gates green. dual-build-cjs-loads was not measured locally and runs in CI's full build.

  6. Out of scope: none filed.

    • check:tenant-chokepoint's SCAN_FILES does not list the new module, as with cut 13's. The moved bodies hold none of the four D8 predicates (4 = 4 at both ends) and build no query. upsertTenantGuard builds the organization fence check from resolveTenantField, which stays in the class. Widening SCAN_FILES would be a gate change, which this card rules out.
    • The next slice by this table is the backend statement fault classifier (about −216).
  7. Landing: this seat runs pr_ready and then automerge_enable through the relay, confirms MERGED on both readings, and records the landing on [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.


Generated by Claude Code

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

Labels

size/l 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