Skip to content

fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) - #20504

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20437-replica-needs-syncurl
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-20437-replica-needs-syncurl

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20437
Clause-②: yes (narrowing)

The Clause-② line above is the claim's (comment 5876481971), copied as it stands. The changeset carries the same value.

Session session_01N8TPEsoJxPsdSdNKGnNGEN (PM dispatch, domain:engine seat 1, mode:subagent), branch claude/issue-20437-replica-needs-syncurl. The branch starts at fc0db22bc and was merged with origin/main at 9bf5e67af in a true merge commit. Every final reading below was taken at head 7bb7b3aee unless it says otherwise.

What changes

A turso config that forces mode: 'replica' on a file: url with no syncUrl (or an empty one) is now refused at both doors, with one message, as triage ruled (5871347046: "Refuse, with one message, at both doors in one PR"):

The message, the same text at both doors:

mode: 'replica' makes this datasource an embedded replica, a local file kept in sync with the remote named in syncUrl, but no syncUrl is set: nothing would ever sync, so it would run as a plain local database that never replicates — the turso driver refuses this configuration when it starts. For an embedded replica, name the remote in syncUrl beside the local file: url: 'file:./data/replica.db' with syncUrl set to the libsql:// or https:// Turso endpoint. For a plain local database, drop mode: 'replica'.

It gives both fixes the ruling prescribes: add syncUrl, or drop mode: 'replica' for a plain local file. It echoes no url and carries no tracker id.

H1: the premise, measured before the change

At the base fc0db22bc, the existing pins that hold today's answer passed: 3 files, 38 passed (-t replica). They were the parity row "file: under a forced mode: 'replica'" (constructor accept, spec accept), the unrecognised-url file's WIDENED FILE: + mode 'replica', no syncUrl cell (constructs, and the rows survive a restart), and the #20200 file's "the rider stays" control. So both schemas accepted the config, and the constructor built it with transportMode = 'replica'. That half of H1 holds as stated.

The runtime half (isSyncEnabled() false, no interval, sync() a no-op) rests on two things. The first is the card's dist probe (the #20200 dev, at dbddf02c1 and again at 2242ad513). The second is the source at the base: connect() builds the sync client only inside if (this.tursoConfig.syncUrl) (turso-driver.ts:1821), sync() returns early on !(this.libsqlClient && this.tursoConfig.syncUrl) (:3522), and isSyncEnabled() is !!this.tursoConfig.syncUrl && this.libsqlClient !== null (:3535). My own dist probe was NOT MEASURED: the session's permission classifier refused writing the probe script to the scratchpad, so I did not re-run it through another channel (see Deviations).

H2: producers, before refusing anything

Census of mode: 'replica' / "replica" across the tree at fc0db22bc, plus every spelling that builds a TursoConfig:

  • examples/, packages/create-objectstack (templates), skills/, hand-written content/docs: zero authors of a forced replica. The only content/docs hit is the auto-generated reference row for the mode enum.
  • Environment mappings: the only TURSO_* / OS_DATABASE_* names read in packages/**/src are OS_DATABASE_URL, OS_DATABASE_AUTH_TOKEN, OS_DATABASE_DRIVER, OS_DATABASE_POOL_MAX, OS_DATABASE_SQLITE_JOURNAL_MODE, TURSO_DATABASE_URL, TURSO_AUTH_TOKEN and TURSO_TOKEN. None of them maps to mode or syncUrl.
  • Programmatic builders: a turso mode reaches the driver only through buildTursoDriverConfig's mode reader (packages/services/service-datasource/src/turso-driver-config.ts:205), which reads an authored datasource.config.mode. The CLI and the standalone host build their default datasource from the env url and token alone.
  • In-repo fixtures that spell mode: 'replica': packages/cli/src/utils/storage-driver.test.ts:478, packages/runtime/src/turso-driver-factory.convergence.test.ts:70/:102 and packages/services/service-datasource/src/__tests__/turso-driver-config.test.ts:49/:60. Every one carries a syncUrl, and none constructs the real driver.
  • objectstack-ai/cloud: NOT MEASURED. Triage names it. The repo is not reachable from this session: REST code search answers "sessions are bound to their configured repositories", and git ls-remote is refused. Attaching it through the session tool was refused by the permission classifier. The seat or the maintainer should census cloud before this lands.

So no shipped in-repo config declares a replica with no remote, and the ruling's needs_decision branch does not trigger on anything measured here.

H4: every mode / url cell, before and after

config before: constructor before: spec / mirror after
file:, no mode, no syncUrl local accept / accept unchanged (a plain local file, not a replica)
file: + forced replica, no syncUrl replica, never syncs accept / accept (the mirror strips mode and judges it local) refused at construction and by the spec, on mode
the same on an uppercase FILE: url the same (the WIDENED cell) accept / accept refused
the same with syncUrl: '' (unset) the same accept / accept refused
the same with timeoutMs the same accept / accept refused
the same with sync, no syncUrl refused, sync message (#20200) 1 issue on sync constructor unchanged (the sync message). The spec now raises 2 issues, sync then mode
libsql:// + forced replica, no syncUrl refused, remote-url message refused on url unchanged. It keeps its url refusal, which already names both ways out (drop mode or set it remote; or a file: url beside syncUrl). Not the same message, and no second refusal
:memory: / file::memory: + forced replica refused, in-memory message refused on url unchanged
a bare path + forced replica refused, unrecognised-url message refused on url unchanged
file: + forced replica + syncUrl replica accept / accept unchanged (control)

The "after" column is pinned by the new test file (the refusal, ORDER and CONTROLS blocks) and by the parity table.

H5: ADR-0087 disposition — registered

The family's entry turso-config-transport-mismatch-refused does not cover this refusal. Its surface enumerates the refused combinations (a remote url beside syncUrl or under a forced local/replica mode, an unrecognised url, an in-memory replica, timeoutMs beside a WebSocket url, and syncUrl under a forced remote mode). A forced replica on a file: url with no syncUrl is not among them, and its acceptanceCriteria name only config.url, config.syncUrl and config.timeoutMs. Its replacement would fit, but the surface an upgrading author greps would not name the shape. So, per the order's H5, a new D3 entry lands with the change: packages/spec/src/migrations/entries/semantic/18.turso-config-forced-replica-without-sync-url-refused.ts, id turso-config-forced-replica-without-sync-url-refused, with its surface, replacement, reason (the measurement, the order of the sibling refusals, and what a stored row now does at boot) and acceptanceCriteria (reported at config.mode). src/migrations/registry.ts was regenerated by gen:migration-registry (311 semantic), never by hand. Patch round 1 corrected the entry's surface. It had named the driver's TursoConfigSchema mirror among the surfaces where this shape "is now refused, on mode". The mirror cannot refuse it, because it declares no mode key and strips an authored one. The surface now names the two doors, the spec contract and the constructor. It says the mirror carries the arm's text for parity but cannot see a forced mode and still accepts the config as a local file (review 5877910448, judgment 4). registry.ts was regenerated again by gen:migration-registry. The existing entry is untouched, because it is #20200's text and stays true. The changeset carries registered turso-config-forced-replica-without-sync-url-refused, and check-adr-0087-registration reads it as [BREAKING+bang+clause-②-narrowing] registered … (new here: …). spec-changes.json and docs/protocol-upgrade-guide.md do not project major-18 entries yet (the sibling entry is absent from both too), and check:spec-changes / check:upgrade-guide are green.

Stored rows (read, not edited): buildTursoDriverConfig forwards a stored row's mode unparsed, so a row in this shape now fails at factory.create. DatasourceConnectionService records it failed-degraded (datasource-connection-service.ts:355/:457), and a test connection answers ok: false with "Failed to build driver: MESSAGE" (datasource-admin-plugin.ts:707). Under ADR-0062 D5 the boot fails fast when objects bind to it, unless OS_ALLOW_DRIVER_CONNECT_FAILURE is set. The changeset's FROM → TO paragraph says so.

Tests

suite at 7bb7b3aee result
@objectstack/driver-turso vitest, whole package 77 files · 2065 passed · 22 skipped · exit 0
@objectstack/driver-turso typecheck (tsc --noEmit) exit 0. tsc --listFilesOnly (pre-merge) counts all 4 touched test files in the program
@objectstack/spec vitest --project local, 3 shards 572 files · 16796 passed · 1 todo (6070 + 5190 + 5536), exit 0 on each shard
@objectstack/spec typecheck (tsc + scripts + check:test-typecheck) exit 0
@objectstack/spec check:generated (pre-merge, after the spec build) "All 15 generated artifacts are up to date"

The 22 skips are the parity table's forced-mode rows for the mirror, which strips mode: 18 before, plus the 4 new mode rows.

  • spec/turso-config-constructor-parity.test.ts: the row "file: under a forced mode: 'replica'" flips from accept to refuse, refusedOn: 'mode'. Three mode rows are added (an uppercase FILE: url, an empty syncUrl, and timeoutMs), plus an accept control, "file: + syncUrl under a forced mode: 'replica'". The row "sync with no syncUrl under a forced mode: 'replica'" now declares issues: 2, a new per-row field (default 1). SYNC_KEY_REFUSALS takes the mode rows, so each of the 4 asserts the constructor's message equals the spec issue's. New floors: mode at least 4, sync-key refusals at least 12.
  • turso-driver-unrecognised-url-refusal.test.ts: the WIDENED FILE: + mode 'replica', no syncUrl cell leaves PRESERVATION, and the restart test keeps only its syncUrl half. The header records why.
  • turso-driver-ignored-sync-key-refusal.test.ts: the "rider stays" control is removed, and the header points to the new file.
  • turso-driver-forced-replica-without-sync-url-refusal.test.ts (new): the refusal as the envelope (code + status) plus its first sentence, on file: and FILE: urls, beside an empty syncUrl, timeout, encryptionKey, and a supplied client (the client stays untouched). It also covers createTursoDriver() and a url-echo check. ORDER: a remote url, :memory:, file::memory:, a bare path and sync each keep their own refusal. CONTROLS: a forced replica beside syncUrl connects, reports sync on and keeps its rows across a restart; a replica selected by syncUrl; file: with no mode; mode: 'local'; :memory:; and detectMode still answers replica.
  • packages/spec/src/data/driver/turso.test.ts: the accept fixture { url: 'file:./data/replica.db', mode: 'replica' } gains a syncUrl. A new block asserts the refusal on mode (the envelope, the first sentence, both ways out), that the url refusals keep their order, the two issues beside sync, both authoring doors (config.mode and validateDriverConfig), and the controls.

Reverse verification, via scripts/ablation-replace.mjs from the committed state, pre-merge head 79fbad660. Each direction was predicted before the run, and all three matched:

  1. Constructor refusal disabled: if (mode === 'replica' && !config.syncUrl) { became if (false && …) {, and the mutation landed (anchor 1 → 0, blob 8c7f7be9 → a0c11cd6). Predicted 16 RED. Got 16 failed / 236 passed: the new file's 8 refusal cases, plus the parity table's 4 constructor verdicts and 4 message pins. ORDER and CONTROLS stayed green. Restored: blob == HEAD, git diff HEAD empty.
  2. One byte of the copy: a doubled space after the constant's first sentence (blob → e70d75a1). Predicted exactly the 4 message pins. Got 4 failed: exactly the 4 parity message pins, while every verdict and first-sentence case stayed green. The byte-equality pin is what holds the copy. Restored the same way.
  3. The spec arm disabled in packages/spec/src/data/driver/turso.zod.ts (blob fdfb4a6f → 0634285e), against the spec's own source-level test. Predicted 3 RED. Got 3 failed / 31 passed: the refusal, the two-issue and the both-doors cases. The order and control cases stayed green. Restored the same way.

The driver tests import the driver from src, so ablations 1 and 2 needed no build. Ablation 3 was read on the spec's own source tests only; the parity table's spec half reads the built spec dist, and that was not re-ablated.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run after the last commit at 7bb7b3aee, lists 11 paths vs merge base 9bf5e67af and 91 commands. Every command ran, with its exit code written to disk before any pipe. --ran reconciliation reads 91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), with 0 UNRUN.

  • NOT MEASURED (exit 3, PREREQUISITE NOT MET): check:dual-build-cjs-loads (dozens of workspace packages have no dist/) and check:type-check-debt (it needs a whole-workspace build). Both are CI's run.
  • Run twice: check:doc-formula-expressions and check:lean-entry-closure first answered exit 3. They are exit 0 after building the @objectstack/lint and @objectstack/objectql closures.
  • Notable readings:
    • check-adr-0087-registration → registered turso-config-forced-replica-without-sync-url-refused (new here), BREAKING, bang, clause-② narrowing;
    • check-changeset-no-major exit 0;
    • check:migration-registry → registry.ts is current (311 semantic, 230 retired-key, 206 retired-def);
    • check:driver-conformance, check:nul-bytes, check:doc-authoring, check:test-source-alias, check:cross-package-test-inputs, check:api-surface ("unchanged"), check:authorable-surface, check:docs, check:spec-changes, check:upgrade-guide and check-empty-changeset → exit 0.
  • Roster families under a touched directory, also run: check-changeset-fixed, spec check:meta-url-spelling, check:authz-resolver, check:error-code-casing and check:filter-alias-parity, all exit 0.
  • Narrowed lint: eslint --no-inline-config --format json over the 10 changed TS files reports 10 files, 0 errors and 0 warnings. The population is eslint.config.mjs's lint object, files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']; the changeset .md is outside it. Invariance holds because the config enables no type-aware linting (no parserOptions.project), so this diff cannot move any untouched file's verdict. The full pnpm lint is CI's.
  • Not run locally, left to CI: the whole-workspace type-check lanes; the Test Core, Dogfood and Build Core jobs; and the downstream suites of @objectstack/service-datasource, @objectstack/runtime and @objectstack/cli. The narrowing is declared: the public surface's bytes are unchanged (check:api-surface and check:authorable-surface are green, and refinements are not in the JSON Schema). The fixture census above finds no consumer fixture that spells the refused shape, and none that constructs the real driver with a forced mode.

Driver-conformance ledger (lanes/engine.md): check:driver-conformance read OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt both before (fc0db22bc) and after (7bb7b3aee). driver-turso is ok on all 10 case-sets both times. No movement.

Deviations (declared)

  • H1 dist probe not re-run. Writing a scratch probe script was refused by the session's permission classifier. The runtime half rests on the card's cited probe and the base source lines above, and the constructor/schema half on the existing pins at the base.
  • Cloud producers not measured (H2): the repo is unreachable from this session, and attaching it was refused. Nothing in-repo triggers needs_decision.
  • Parity harness: a per-row issues count (default 1) was added for the one row where the spec now raises two issues. The alternative was to suppress the mode issue when sync also fires, which would couple the two arms. Raising both matches how the schema reports every other independent defect.
  • Issue path mode: the triage ruling names the message, not the key. mode was chosen by the sibling convention (the sync refusal sits on the present, unhonourable key). TursoTransportIssue.path widens to include 'mode' in both copies (module-local types, no export).

Acceptance notes

Patch round 1 (text only, head 5dfa45e9f)

This round follows the at-tier review 5877910448 (PASS at 7bb7b3aee; judgment 4 names one wrong clause). It makes two edits and changes no code, test, schema text or message:

  1. The D3 entry turso-config-forced-replica-without-sync-url-refused: its surface no longer lists the driver mirror among the refusing surfaces. It now names the spec contract and the constructor, and says what the mirror does (the H5 note above). registry.ts was regenerated by gen:migration-registry, and its hunk equals the entry's, re-indented. The sibling family entry turso-config-transport-mismatch-refused is not edited here; the seat carries its same overstatement as a note.
  2. packages/drivers/driver-turso/README.md: the list of constructor refusals gains one line each for syncUrl under a forced mode: 'remote', sync with no syncUrl, and a forced mode: 'replica' with no syncUrl, in the list's own style. This is a declared deviation (docs text, outside the claim's letter).

The head is 6a076723d (the text commit) plus a true merge of origin/main at 4a1df1965. The merge touched no migrations or turso path, and gen:migration-registry after it wrote no change. Readings at 5dfa45e9f, each exit code recorded before any pipe:

  • pnpm --filter @objectstack/spec check:migration-registry exit 0: registry.ts is current (311 semantic, 230 retired-key, 206 retired-def);
  • pnpm check:adr-0087-registration exit 0: [BREAKING+bang+clause-②-narrowing] registered turso-config-forced-replica-without-sync-url-refused (new here …);
  • spec check:spec-changes, check:upgrade-guide, check:doc-authoring, check:nul-bytes and check-changeset-no-major all exit 0;
  • spec vitest --project local over src/migrations, src/conversions and src/data/driver: 22 files, 1044 passed, exit 0.

dispatch-gates --commands at this head derives the same 91 commands as round 0 (12 paths vs merge base 4a1df1965; the README adds no family). The full union was not re-run for this text-only delta. The round-0 union at 7bb7b3aee stands, and CI measures this head.

Patch round 2 (merge + form D), head 519cfbce5

This round follows the maintainer's ruling recorded in 5883364572, 「20504 不考虑 cloud 现有数据」: the cloud producer census stays NOT MEASURED, waived by the maintainer, and nothing in objectstack-ai/cloud was read or edited. The round changes no code, test, schema text, refusal message or changeset. The changeset's level and the Clause-② line do not move.

  1. The merge. origin/main at 1c761c0d7 was merged into the branch in a true merge commit, 04be827ce (parents 5dfa45e9f and 1c761c0d7), by bash scripts/pm/os-regen-merge.sh. There was no rebase and no force-push. Since the old base 4a1df1965, main had moved over two of this PR's files, and both auto-merged with no conflict:

    • packages/spec/src/migrations/registry.ts (generated): stages 4–6 of the guidance rewrite restated other entries, and new entries landed.
    • packages/drivers/driver-turso/src/turso-driver.ts: the $empty value-shape resolver wiring and the $empty presence-flag case. The merged file carries both sides: setDeclaredValueShapeResolver and case '$empty': sit beside REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL.

    packages/spec/src/data/driver/turso.zod.ts did not move on main in that window. The merge left no os-regen deferral.

  2. Regeneration, with the repo's own tooling. pnpm --filter @objectstack/spec check:migration-registry read the textually merged registry.ts as current (315 semantic, 232 retired-key, 206 retired-def), and pnpm --filter @objectstack/spec gen:migration-registry then wrote no change. After the entry edit in item 3, gen:migration-registry ran again, and its registry.ts hunk is the entry's hunk re-indented (+4 / −1 in each file). spec-changes.json and docs/protocol-upgrade-guide.md still project no major-18 entry. check:spec-changes, check:upgrade-guide and pnpm --filter @objectstack/spec check:generated ("All 15 generated artifacts are up to date", after the spec build at this head) are green, so no other artifact needed regenerating. Against the new merge base the net diff is 12 files, +539 / −43.

  3. Form D in the entry's reason. The file is 18.turso-config-forced-replica-without-sync-url-refused.ts, and reason is the why: line os migrate meta prints. The id, surface, replacement, acceptanceCriteria, the registration and the ADR-0087 disposition (registered turso-config-forced-replica-without-sync-url-refused) are unchanged. Only the opening moves:

    • Before: "#20437. An embedded replica is a local file kept in sync with the remote named in syncUrl. A forced mode replica with no syncUrl parsed clean at authoring, …"
    • After: "An embedded replica is a local file kept in sync with the remote named in syncUrl, so a replica is defined by its remote. The ruling of 2026-09-28 weighed refusing this shape against documenting a replica with no remote as a local mode, and refused it: with no remote there is no replica mode to document, only a declaration nothing honours. A forced mode replica with no syncUrl parsed clean at authoring, …"

    The rest of reason is byte-identical. The date and the lesson are triage's ruling (5871347046): an embedded replica is defined by its remote, so there is no legitimate local replica mode to document instead. The four author-shown fields were evaluated with their string literals joined, and scanned with the CLI pin's detector (# followed by 4 or 5 digits). Each field reads 0 now. The lit control is the same probe over the entry at 5dfa45e9f, which reads 1 in reason. No test asserted the old sentence, so nothing was re-pinned. The CLI pin migrate-meta-engine-guidance.test.ts selects entries by id prefix, and turso- is not one of its covered families, before or after stage 7 on main. No other test quotes the text. The sibling turso-config-transport-mismatch-refused still opens with a tracker number, and it is untouched, as ordered.

Readings at 519cfbce5, the head measured. Each exit code was recorded before any pipe.

  • Builds. turbo run build over the @objectstack/driver-turso, @objectstack/lint and @objectstack/objectql closures: 15 tasks, exit 0.
  • @objectstack/driver-turso. Vitest over the whole package: 78 files, 2108 passed, 22 skipped, exit 0. Typecheck exit 0, and tsc --listFilesOnly counts all 4 touched test files.
  • @objectstack/spec. Vitest --project local in 3 shards: 574 files, 16884 passed, 1 todo (6106 + 5220 + 5558), exit 0 on each shard. Typecheck (tsc, scripts and check:test-typecheck) exit 0.
  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack lists 12 paths vs merge base 1c761c0d7 and 91 commands, and all 91 ran. --ran reads "91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". The 2 NOT MEASURED are check:dual-build-cjs-loads and check:type-check-debt, both PREREQUISITE NOT MET because they need a whole-workspace build. CI runs both.
  • Gates the order names. pnpm check:doc-authoring exit 0. pnpm check:adr-0087-registration exit 0: the self-test holds 441 assertions, and the gate reads [BREAKING+bang+clause-②-narrowing] registered turso-config-forced-replica-without-sync-url-refused (new here …). pnpm --filter @objectstack/spec check:migration-registry exit 0 (315 semantic). check:spec-changes, check:upgrade-guide, check:release-spec-changes, check-changeset-no-major --base origin/main and check:nul-bytes exit 0. check:doc-authoring does not read a migration entry's prose fields: patch round 1 recorded it green at 5dfa45e9f with the number still present. So the proof for item 3 is the field census above.
  • Roster families under a touched directory. check-changeset-fixed, spec check:meta-url-spelling, check:authz-resolver, check:error-code-casing and check:filter-alias-parity, all exit 0.
  • Narrowed lint. eslint --no-inline-config --format json over the 10 changed TS files reports 10 files, 0 errors and 0 warnings. The population is eslint.config.mjs's files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']. The config enables no type-aware linting, so this diff cannot move an untouched file's verdict.
  • Main moved after the merge. It is now at f11b5f20a, stage 7 of the guidance rewrite, which touched registry.ts and other families' entries. A driver-free probe (a bare shared clone, merge-tree --write-tree) merges this head with it cleanly. build-migration-registry.ts --check over the probed tree reads registry.ts as current (315 semantic). The branch was not merged again this round.

Deviations (declared). The branch took two pushes this round, the merge commit and then the reword commit, following AGENTS.md's push-before-every-long-step rule. The merge commit carries no trailer. The reword commit ends with the model-free trailer pair.


Generated by Claude Code

…rl at authoring and at construction

A turso config that forces mode 'replica' on a file: url with no syncUrl
(or an empty one) was accepted by both TursoConfigSchema copies and by
the constructor, and ran as a plain local database that never synced.
Both doors now refuse it with one message: the spec contract on mode,
and new TursoDriver with VALIDATION_ERROR / 400 after the sync refusal,
its text a module constant pinned byte-equal by the parity table.

The parity row, the unrecognised-url WIDENED cell and the rider control
flip to refused; a new constructor test file and spec tests pin the
refusal, its order and its width. ADR-0087: a new D3 entry
turso-config-forced-replica-without-sync-url-refused, registry
regenerated.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-turso, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/automation/jobs.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/automation/webhooks.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/protocol/kernel/lifecycle.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))

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

  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-0.mdx (via timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-4.mdx (via TursoDriverConfig (symbol, a top-level interface), timeoutMs (literal, a string literal in TursoTransportIssue))
  • content/docs/releases/v17/17-5.mdx (via syncUrl (literal, a string literal in TursoTransportIssue), timeoutMs (literal, a string literal in TursoTransportIssue))

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
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-turso/README.md) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 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 — 140 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 1c761c0d7100ecdcbb293fe090a5f9212283b835 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 89e4f10130d53b343c206f080b38341f7d04daf7 — the merge of head 519cfbce5fd4ea5de7eca841604225d389f15bb2 into base 1c761c0d7100ecdcbb293fe090a5f9212283b835, 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 89e4f10130d53b343c206f080b38341f7d04daf7 && git checkout 89e4f10130d53b343c206f080b38341f7d04daf7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1c761c0d7100ecdcbb293fe090a5f9212283b835 519cfbce5fd4ea5de7eca841604225d389f15bb2 && git checkout -B drift-repro 1c761c0d7100ecdcbb293fe090a5f9212283b835 && git merge --no-ff 519cfbce5fd4ea5de7eca841604225d389f15bb2

node scripts/docs-audit/affected-docs.mjs --json 1c761c0d7100ecdcbb293fe090a5f9212283b835

⚠️ 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 1c761c0d7100ecdcbb293fe090a5f9212283b835 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7bb7b3aee323d8db6c1fae8bd614bf2c9b793c5e
Local-runs: none

Read-only, at tier, on PR #20504 for card #20437 (rider of #20200), branch claude/issue-20437-replica-needs-syncurl. Inputs: the card body and all three comments (triage 5871347046, claim 5876481971, os-dev-report 5877637196), the PR body and file list, the net diff against main at merge base 9bf5e67 (11 files, +517 / −43, one code commit 79fbad6 plus a true merge of origin/main), the head's check-runs, and for the pattern PR #20447 (#20200) and PR #20199 (#19977). Not fed: the dispatch order or the seat's own conclusions. Sources at the head were read with git show / git grep only; no worktree, build, test or gate was run. The cloud repo is not reachable from this session either (a REST GET at objectstack-ai/cloud answers 403, session-bound), so nothing below claims a cloud reading.

① Derived judgments

1. The refused shape, at both doors — right. The spec arm (packages/spec/src/data/driver/turso.zod.ts, tursoTransportIssues) is a new if (mode === 'replica' && !hasSyncUrl) after the in-memory arm, inside the mode !== 'remote' branch; mode is the forced one or the url-selected one, and with no mode a replica is selected only by syncUrl, so the arm fires only for a FORCED replica, on a url the three arms above it did not take (a file: url that is not in-memory), with syncUrl absent or empty (hasSyncUrl is a non-empty string). The constructor (turso-driver.ts) adds if (mode === 'replica' && !config.syncUrl) refuseIgnoredSyncKey(REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL) where mode is detectMode(config) (the forced mode, else syncUrl selects replica), placed after localEngineDefect, the two timeout refusals and the two #20200 sync-key refusals, and before super(); refuseIgnoredSyncKey throws the ADR-0112 envelope (VALIDATION_ERROR, status 400). Cells, before → after, read from the base and head sources and the pins:

  • file: url, forced replica, no syncUrl: constructor built a replica that never synced (base connect() opens the sync client only under if (this.tursoConfig.syncUrl) at turso-driver.ts:1868 at the head, :1821 at the base; sync() returns early and isSyncEnabled() is false without syncUrl, base :3522 / :3535 as the PR body cites) / spec accepted / mirror accepted → refused at construction and by the spec, one custom issue on mode. Right.
  • the same on an uppercase FILE: url, with syncUrl: '', or beside timeoutMs / timeout: the same before → refused. Right (the url is a file: url in any letter case; an empty syncUrl is unset at both doors; timeoutMs is judged only in remote mode).
  • the same beside sync and no syncUrl: refused on sync since driver-turso: new TursoDriver accepts syncUrl / sync under mode: 'remote' and ignores them — isSyncEnabled() answers true, no sync runs, and sync() rejects SYNC_NOT_SUPPORTED #20200 / spec 1 issue on sync → constructor unchanged (the sync message, first); the spec raises 2 issues, sync then mode. Right, and the order is pinned by both the spec test and the parity row.
  • libsql:// (any remote scheme) under forced replica, no syncUrl: refused on url before and after (constructor localEngineDefect remote-url; spec's first arm) — unchanged, and that message already names both ways out (drop mode or set mode: 'remote'; or a file: url with the remote named in syncUrl). Right. :memory: / file::memory: under forced replica: the in-memory refusal, unchanged. A bare path under forced replica: the unrecognised-url refusal, unchanged. Right.
  • file: with no mode (with or without syncUrl), mode: 'local', :memory:, and file: + forced replica + syncUrl: accepted before and after (controls pinned in the new file, the parity table and the spec test). Right. Nothing beyond the ruling's shape is refused.
  • The message is the same text at both doors: the six string pieces of the spec arm, the mirror arm and the driver constant are identical by reading, and the parity table's SYNC_KEY_REFUSALS pin now compares the constructor's message with the spec issue's for the four mode rows, so the spec/constructor equality is held by a test as driver-turso: new TursoDriver accepts syncUrl / sync under mode: 'remote' and ignores them — isSyncEnabled() answers true, no sync runs, and sync() rejects SYNC_NOT_SUPPORTED #20200's two are (H3 pattern). It names both fixes (syncUrl beside the local file; drop mode: 'replica'), echoes no url and carries no issue id. Right.

2. The driver mirror — right, with one caveat. The mirror (packages/drivers/driver-turso/src/spec/turso.zod.ts) is a plain z.object with no mode key, so zod strips an authored mode before its superRefine; cfg.mode is always undefined there and tursoTransportModeOf never answers replica without syncUrl. The arm is unreachable through the mirror: TRUE. Carrying it is parity with the mirror's documented policy (its header: the forced-mode branches "are kept so the two copies stay byte-identical"), and the alternative — a mirror that diverges textually from the spec — is what the parity table exists to prevent. Caveat, pre-existing and not this PR's: the parity table skips the mirror for every forced-mode row, so no test pins the mirror's copy of any forced-mode text, this one included; it is held equal by reading only.

3. The flipped pins — right. (a) Parity row "file: under a forced mode: 'replica'" was ctor: 'accept' with no refusedOn (the old answer); it is now refuse, refusedOn: 'mode', joined by three mode rows (FILE:, syncUrl: '', timeoutMs) and a new accept control "file: + syncUrl under a forced mode: 'replica'". (b) turso-driver-unrecognised-url-refusal.test.ts: the WIDENED cell "FILE: + mode 'replica', no syncUrl → replica" and the ['no syncUrl', false] half of the restart test were the old answer; they leave, the syncUrl half stays, and the header records the #20437 re-refusal on other grounds. (c) turso-driver-ignored-sync-key-refusal.test.ts: "the rider stays" was the old answer; removed, header repointed to the new file. The new set holds the new answer: the new file's 8 refusal cases (envelope, first sentence, both ways out; FILE:, empty syncUrl, timeout, encryptionKey, a supplied client left untouched, createTursoDriver, no url echo), 5 ORDER cases, 6 CONTROLS, plus the parity rows and the spec test's five cases. Per-row issues count: sound — schemaVerdict reports the first issue's path and the issue count; issues: 2 on the one row where the spec raises sync then mode keeps the exact-one assertion on every other refused row, the constructor pin compares against the first issue (the sync message the constructor throws), and the mirror comparison is skipped for that row as for every forced-mode row; the spec's own test pins the second issue's identity and order. Floors: mode at least 4 and SYNC_KEY_REFUSALS at least 12 are the exact counts (3 syncUrl + 5 sync + 4 mode). The 22 mirror skips are the 22 forced-mode rows (18 at the base + 4).

4. ADR-0087 D3 entry — new entry required: right; text: one clause wrong. The family entry turso-config-transport-mismatch-refused was read at the head: its surface enumerates the remote-url, unrecognised-url, in-memory-replica, timeoutMs-on-WebSocket and syncUrl-under-remote shapes and its acceptanceCriteria name only config.url, config.syncUrl and config.timeoutMs; a forced replica with no syncUrl is not among them, so not-required (already-registered …) would not hold and registered turso-config-forced-replica-without-sync-url-refused is the honest disposition (the gate also requires the id to be new in the diff, which it is). registry.ts: generator output, no hand edit — the diff is exactly the entry file's leading // run and literal, re-indented four spaces, inserted in id order (after translation-per-app-settings-platform-only, before turso-config-timeout-unit-in-key, which is how build-migration-registry.ts sorts); the registry's 311 semantic ids match the 311 entry files. The replacement, reason and acceptanceCriteria are right (both ways out; the measurement; sibling order; the stored-row boot outcome; reported at config.mode). Wrong cell: the surface names "the published TursoConfigSchema mirror of @objectstack/driver-turso" among the surfaces where this shape "is now refused, on mode". The mirror does not and cannot refuse it (judgment 2); it accepts the config as a local file. The changeset says this correctly; the entry does not. This is a text defect in published registry data, not an accept-set defect: the fix is a one-clause edit (drop the mirror from surface, or say it carries the text and cannot see a forced mode) plus gen:migration-registry — text-only, the #20200 patch-round shape. The family entry carries the same overstatement for its forced-mode shapes, so the sibling would take the same edit.

5. TursoTransportIssue.path — module-local: right. In both copies TursoTransportIssue is an interface with no export; the spec barrel's export * from './turso.zod' and the driver index's export * from './spec/turso.zod.js' re-export only exported names. No exported type changes; the JSON-Schema projection carries no refinement, so check:authorable-surface / check:api-surface "unchanged" is the expected reading. REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL is an unexported module constant, as #20200's two are.

Producer census (in-repo), re-read at the head: no non-test file authors mode: 'replica' (quoted or bare) outside the two schema copies, the driver, the README's prose and the changesets; examples/, packages/create-objectstack, skills/ and hand-written content/docs have no author of the shape; the only syncUrl hits under content/ are the generated reference and a release page. The env names read in packages/**/src are exactly the eight the PR body lists (the other TURSO_* hits are identifiers, not env vars); none maps to mode or syncUrl. The three out-of-package fixtures with mode: 'replica' (cli storage-driver.test.ts:478, runtime turso-driver-factory.convergence.test.ts:70/:102, service-datasource turso-driver-config.test.ts:49/:60) all carry a syncUrl. buildTursoDriverConfig forwards a stored row's mode unparsed (turso-driver-config.ts:205). All TRUE.

Changeset sentences: front matter @objectstack/spec minor, @objectstack/driver-turso minor — TRUE to the launch-window rule. The Clause-②: yes (narrowing) line and "no key added, removed or renamed, no exported symbol moves" — TRUE. "was accepted by the spec schema, the published mirror and new TursoDriver()" — TRUE at the base. "Measured on the built driver before this change, with and without sync" — TRUE of the card's cited dist probe (taken before #20200 landed; at this PR's base the sync variant is already refused on sync, and the driver's own test header says so); from a consumer's side both changes ship in the same unreleased window, so the sentence holds. "BREAKING … shipped as minor" — TRUE. The two doors, the constructor envelope before any client — TRUE. "a test holds the constructor's copy equal … byte for byte" — TRUE. Sibling order and the two-issue spec reading — TRUE. The mirror "declares no mode key and strips an authored one … the spec contract and the constructor are the two doors" — TRUE. The FROM → TO table — TRUE. The stored-row paragraph (factory.create throws; failed-degraded at datasource-connection-service.ts:355 / :457; test connection ok: false "Failed to build driver" at datasource-admin-plugin.ts:707; ADR-0062 D5 fail-fast unless OS_ALLOW_DRIVER_CONNECT_FAILURE) — TRUE by reading. "Blast radius … NOT measured [out of repo] and not claimed to be zero" — TRUE. One adr-0087 marker, registered with the new id — TRUE.

PR-body sentences: "Fixes #20437" and the Clause-② line copied from claim 5876481971 — TRUE. Branch from fc0db22 (a main commit), true merge with 9bf5e67, head 7bb7b3a — TRUE. What changes (arm position, issue on mode, constructor after the sync refusal before super(), the constant, the helper, the mirror unreachable) — TRUE. The message block — byte-equal to the three copies. H1's source-line readings — TRUE (verified at both fc0db22 and 9bf5e67). H2 — TRUE (above); "cloud NOT MEASURED" — declared, and confirmed unreachable from here. H4 table — every cell TRUE. H5 — TRUE except that the new entry's surface misnames the mirror (judgment 4); "spec-changes.json and the upgrade guide do not project major-18 entries (the sibling is absent too)" — TRUE (zero hits for any major-18 id in either). Tests: "22 skips = 18 + 4" — TRUE; the counts, the three ablations (16 = 8 + 4 + 4; the 4 message pins; 3 spec cases) are consistent with the diff's case inventory, dev-run, not re-run here. Gates — dev-run; the head's check-runs are the verdict (below). Acceptance notes: the README's refusal list names only the three url refusals — TRUE; the spec mode describe unchanged — TRUE; the family entry's "nothing the constructor accepts is refused" still true — TRUE.

② Semver level

@objectstack/spec and @objectstack/driver-turso minor, BREAKING, Clause-②: yes (narrowing) — matches what the diff publishes. yes: the published TursoConfigSchema (reachable through @objectstack/spec/data) changes its accept set, and the driver's published constructor changes its accepted set with it; (narrowing): one combination class leaves both accept sets and nothing enters them — no new export, no new key, no describe change, no JSON-Schema change. The D3 entry is data added to the already-published MIGRATIONS_BY_MAJOR registry, the prescribed ADR-0087 carrier of a narrowing, not a Clause-② widening (the closed pair admits one arm, and every registered narrowing would otherwise be both). BREAKING is carried three ways the gate reads — the ! in the title, the bold BREAKING in the body, the (narrowing) arm — and ships as minor under check-changeset-no-major's launch-window rule, as #20199 and #20447 did. The changeset states the FROM → TO mapping and the one-line fixes, and the disposition marker. skip-changeset does not apply. Both PR-body and changeset Clause-② lines carry the same value as the claim. Level: right.

③ Boundary flags

open_questions: none declared; none found.

Deviations, each answered:

  1. Own dist probe NOT MEASURED — answered: the runtime half rests on the fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) #20447 H1 dist-probe table (merged, its RIDER row is this shape) and on the base source lines, which this review re-read at fc0db22 and 9bf5e67 (connect() opens the sync client only under syncUrl; sync() and isSyncEnabled() both test syncUrl first). The constructor/schema half rests on the base pins. Sufficient.
  2. Cloud producers NOT MEASURED — escalated to the seat. Triage made "measure the producers first (examples, cloud)" a precondition and named the shipped-config outcome needs_decision. The in-repo half is measured (zero producers, confirmed above); the cloud half is not, and a not-measured is not a measured-zero. The reviewer could not measure it either (403, session-bound). What a cloud deployment holding such a stored datasource row sees after this lands: no change at load (rows are not re-parsed against the schema; the ADR-0087 chain replays only conversions), then at driver build factory.create throws the refusal, the connection service records failed-degraded with the message (which names both fixes), a test connection answers ok: false, and under ADR-0062 D5 boot fails fast if objects bind to or route to that datasource unless OS_ALLOW_DRIVER_CONNECT_FAILURE is set — loud, not silent, but a break of a running deployment, which is exactly what triage routed to needs_decision. Recommendation: hold landing until a cloud census is recorded on the card — a grep of the cloud repo's shipped datasource configs and env-to-config mappings for a replica mode with no syncUrl (minutes, by the seat in a session with the repo attached or by the maintainer) — or the maintainer waives it in writing; a hit routes to needs_decision per triage; a miss lands the PR as declared. Note the residual either way: a repo census cannot see tenant-stored rows, and their failure mode is the loud D5 path above, the same posture fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) #20447's stored-row refusals landed with.
  3. Issue path mode — right (judgment 5; sibling convention; the key that cannot be honoured; module-local widening).
  4. Parity issues count — sound (judgment 3).
  5. The third pin (rider control) removed — right: a driver-turso test inside the claim's "tests in packages/spec and driver-turso"; it held the old answer; the new file holds the new.
  6. TursoDriverConfig.mode TSDoc and file header extended — inside the surface, and the text is true.
  7. No labels written beyond the assignee — fine; the five labels on the PR are the labeler's.
  8. Commit trailers — the code commit 79fbad6 ends with the model-free pair (Co-authored-by: Claude and Claude-Session), as AGENTS.md asks; the merge commit carries none, which the squash queue does not land. No breach.

Out-of-scope notes: (a) the driver README's refusal list names neither this refusal nor #20200's two — TRUE, incomplete not false; it can ride the text patch of judgment 4 or a docs follow-up (carrier: the seat). (b) A forced mode: 'local' beside syncUrl — the inference is confirmed by reading: toKnexConfig has no local-vs-replica arm (both open the file: path), connect() builds the sync client, syncs on connect and starts the interval whenever syncUrl is set in a non-remote mode, and isSyncEnabled() answers true — so a declared local database replicates and only the transportMode label differs. That is the same ADR-0049 class (c) shape as this card (a declared mode the runtime ignores), accepted by both schemas and pinned accepted by the parity control "file: + syncUrl under a forced mode: 'local'"; recommend the seat file it bare, as #20437 was filed from #20200. Not this PR's.

Text follow-up recommended before release, not a contract defect: the D3 entry's surface mirror clause (judgment 4), text-only with gen:migration-registry; it can ride the round that records the cloud census.

Check-runs on the head, final read at the end of this review (2026-09-28, after 20:26Z): 34 check-runs — 29 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke: path- or opt-in-skipped), 2 still in progress (Test Core 1/6 and Test Core 5/6), 0 failing. Among the successes: Check Changeset, Lint & Repo Gates (the changeset, ADR-0087 registration, migration-registry and generated-artifact gates run there), Spec property liveness, Governed Surface Queue Guard, Type Check · source / consumer / debt ledger / workspace, TypeScript Type Check, Build Core, Dogfood Regression Gate 1/3–3/3 and rollup, Dogfood Verify CLI, Temporal Conformance, Test Core 2/6, 3/6, 4/6 and 6/6, and the four PR-shape guards. The two in-progress Test Core shards are recorded as in progress; the seat reads them green before queueing.

Implemented-by: claude/issue-20437-replica-needs-syncurl
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

…ors; the README lists the sync-key refusals

The new D3 entry's surface named the driver's TursoConfigSchema mirror
among the surfaces that refuse a forced mode 'replica' with no syncUrl.
The mirror declares no mode key and strips an authored one, so it
cannot see a forced mode: it carries the arm's text for parity only.
The surface now names the two doors (the spec contract and the
constructor) and says what the mirror does. registry.ts regenerated by
gen:migration-registry.

The driver README's list of constructor refusals named only the url
refusals; it now also names the three sync-key refusals (syncUrl under
a forced remote mode, sync with no syncUrl, a forced replica with no
syncUrl). Text only.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5dfa45e9f27f2b03eee5f3c3e681f8bfcc7f0820
Local-runs: none

Delta review, read-only, at tier, on PR #20504 for card #20437, branch claude/issue-20437-replica-needs-syncurl, after the PASS 5877910448 at 7bb7b3a. Inputs added: the patch-round-1 os-dev-report 5878055796 on the card, the edited PR body, the diff 7bb7b3a..5dfa45e, and the check-runs on the new head (read last). The new head is the text commit 6a07672 (parent 7bb7b3a) plus a true merge of origin/main 4a1df19. Sources were read with git show, git diff, git log and a read-only git merge-tree --write-tree replay; no worktree, build, test or gate was run. Nothing was posted.

① Derived judgments

1. The PR's own delta is text only — right, and every judgment of the prior record carries over. git diff 7bb7b3aee..6a076723d is three files, +22 / −6: the D3 entry's surface (3 lines → 5), the matching registry.ts hunk (the same 3 → 5, re-indented), and 12 new lines in packages/drivers/driver-turso/README.md. No code, test, schema text or message moved: both turso.zod.ts copies, turso-driver.ts, the four test files, turso.test.ts and the changeset are byte-identical to 7bb7b3a. The PR's net diff against the new merge base (4a1df19..5dfa45e, 12 files, +533 / −43) equals the net diff at the old base plus the text commit (9bf5e67..6a07672) line for line, index lines aside. So judgments 1, 2, 3 and 5 of 5877910448 (the refused shape at both doors, the unreachable mirror arm kept for parity, the flipped pins and the issues count, the module-local TursoTransportIssue.path), the in-repo producer census and the changeset and PR-body TRUE/FALSE audit stand unchanged on this head.

2. Judgment 4's wrong clause — corrected, and the corrected clause is TRUE. The surface now reads: data.TursoConfig and the TursoDriver constructor of @objectstack/driver-turso — refused on mode at authoring and at construction; and "The published TursoConfigSchema mirror of @objectstack/driver-turso carries the same text for parity but declares no mode key and strips an authored one, so it cannot see a forced mode and still accepts the config as a local file". Each clause checks against the sources at the head: the mirror's TursoConfigSchema is a plain z.object with no mode key, so zod strips an authored mode before its superRefine; tursoTransportModeOf(url, hasSyncUrl, undefined) answers local for a file: url with no syncUrl; no arm fires for a file: url in local mode and no sync is present, so the parse succeeds — the mirror accepts the config as a local file. The mirror carries the arm byte for byte and is exported through the driver's export * from './spec/turso.zod.js', so "published" and "carries the same text for parity" are both TRUE. The two doors named are the two that refuse. surface still carries no backtick (the upgrade-guide renderer's constraint). replacement, reason and acceptanceCriteria are untouched and stay right. The sibling family entry keeps its own overstatement, by order; carried as a note (③).

3. registry.ts is exactly the generator's projection — right. Projected by build-migration-registry.ts's rules (the entry file's unbroken leading // run plus the object literal, each line indented four spaces, the trailing ; replaced by ,), the entry at the head is a 44-line block that occurs exactly once in registry.ts at the head, byte for byte, between translation-per-app-settings-platform-only and turso-config-timeout-unit-in-key — the id order the generator sorts by. The round's registry.ts hunk is the same 3 → 5 surface lines as the entry's, re-indented; nothing else in the file moved. 311 semantic ids in the registry against 311 entry files, as before. No hand edit.

4. The README lines — TRUE against the constructor, in the list's style. The new paragraph and three items follow the existing url-refusal list and precede "You can also force a specific mode". Against turso-driver.ts at the head: (a) "syncUrl under a forced mode: 'remote', where the remote client never receives it. For a remote database, drop syncUrl (and sync)" — the constructor's if (mode === 'remote' && config.syncUrl) with REMOTE_MODE_SYNC_URL_REFUSAL, whose text prescribes exactly "drop syncUrl (and sync)"; createRemoteClient forwards no syncUrl (#20200). TRUE. (b) "sync with no syncUrl (or an empty one), in any mode, where nothing reads it. Set syncUrl, or remove sync" — if (config.sync && !config.syncUrl), every mode, an empty syncUrl unset, and the sync message's own prescription. TRUE. (c) "a forced mode: 'replica' with no syncUrl (or an empty one), which would never sync and would run as a plain local database. Name the remote in syncUrl beside the file: url, or drop mode for a local database" — if (mode === 'replica' && !config.syncUrl), forced only (with no mode, detectMode selects replica only beside syncUrl), and the message's two ways out. TRUE. "three sync settings that nothing would honour, each with the message @objectstack/spec's TursoConfigSchema gives at authoring" — the three constants are pinned byte-equal to the spec issues by the parity table. TRUE. The envelope named (VALIDATION_ERROR / 400) is the one refuseIgnoredSyncKey throws. Style: the same bullet-and-semicolon list form and the same intro shape as the url list above it; no issue id in the text. Docs text outside the claim's letter, declared as a deviation in the report and the PR body — a README the diff's own refusals made incomplete, so the edit is in scope of the change it documents.

5. The merge — clean, nothing of this PR's touched. origin/main from 9bf5e67 to 4a1df19 is six first-parent commits (#20487, #20496, #20495, #20475, #20498, #20501); git diff --stat 9bf5e67af 4a1df1965 over the PR's twelve paths is empty, and over packages/spec/src/migrations/ is empty, so main added no registry entry and touched no turso or migrations path. A read-only replay, git merge-tree --write-tree 6a076723d 4a1df1965, yields tree 5524a734429ceb8e03a9afb684194e413a9898f8, identical to the head commit's tree: the merge commit is exactly the conflict-free replay of its two parents, with no hand edit. The PR body's "the merge touched no migrations or turso path, and gen:migration-registry after it wrote no change" is therefore TRUE by construction.

6. Report and PR-body sentences, this round. Report 5878055796: text only, the three files, the corrected surface wording, "registry.ts regenerated … its hunk equals the entry hunk", the README's three items, the sibling untouched, the head composition, "the merge touched no migrations or turso path" — all TRUE. Its readings (check:migration-registry 311 semantic; check:adr-0087-registration registered, new here, BREAKING + bang + clause-② narrowing; spec vitest over migrations, conversions and data/driver 22 files 1044 passed; the same 91 derived commands over 12 paths; the full union not re-run for a text-only delta) are dev-run and consistent with the diff; CI on this head is the gate (below). Deviations: README outside the claim's letter — right, see judgment 4; worktree recreated at 7bb7b3a — housekeeping; full union not re-run — declared, and CI covers it; the text commit carries the model-free trailer pair and the merge commit none — as at round 0, no breach. PR body: the H5 note (the surface had named the mirror as refusing; the mirror cannot; it now names the two doors and says what the mirror does; registry.ts regenerated again; the existing entry untouched) — TRUE. The Acceptance note ("Patch round 1 adds this refusal and #20200's two sync-key refusals, one line each") — TRUE in substance; each is one list item of two or three wrapped lines. The "Patch round 1" section (follows 5877910448 judgment 4; two edits; no code, test, schema text or message; the head composition; the readings; "the round-0 union at 7bb7b3a stands, and CI measures this head") — TRUE, with the readings dev-run as stated. "Every final reading below was taken at head 7bb7b3a unless it says otherwise" stays consistent because the new section says otherwise. The Clause-② line, the changeset and its marker are unchanged.

② Semver level

Unchanged and still right: @objectstack/spec and @objectstack/driver-turso minor, BREAKING, Clause-②: yes (narrowing). This round moves published data text (the D3 entry's surface, in the already-published registry) and a README; it adds no export, no key, no describe, no JSON-Schema change, and narrows nothing further. The changeset is byte-identical to round 0 and matches what the diff publishes; the disposition marker registered turso-config-forced-replica-without-sync-url-refused still names an id that is new in this diff. skip-changeset does not apply.

③ Boundary flags

open_questions: none declared; none found.

Round-0 flags carry as answered in 5877910448, with one change of state:

  • ③.2, the cloud producer census — carried as escalated, unchanged, not re-judged here. It stays held by the seat: a cloud census recorded on the card, or a written waiver by the maintainer, before landing; a hit routes to needs_decision per triage 5871347046.
  • The text follow-up 5877910448 recommended (the entry's surface mirror clause) is done in this round (judgment 2); that item is closed.

This round's flags: (1) the README edit outside the claim's letter — answered, right (judgment 4). (2) The full gate union not re-run on a text-only delta — answered: the union at 7bb7b3a stands for the unchanged code, and the head's check-runs are the verdict for this head. (3) The merge — answered, clean (judgment 5).

Out-of-scope notes carried: (a) the sibling entry turso-config-transport-mismatch-refused carries the same mirror overstatement for its own forced-mode shapes — carrier: the seat, noted, not this PR's by order; a one-clause text edit plus gen:migration-registry whenever the seat next touches it. (b) The forced mode: 'local' beside syncUrl finding (a declared local database that replicates) — carried from round 0; recommend the seat file it bare.

Check-runs on the new head, final read at the end of this review (2026-09-28, after 20:41Z): 39 check-runs on 5dfa45e — 22 success, 5 skipped, 12 still in progress, 0 failing. The 6a07672 runs were cancelled by the next push and are not judged. Two events ran on this head (the push and the PR-body edit), so the PR-shape guards, Check Changeset, Auto Label and Check PR Size appear twice; every concluded pair is success or a path/event skipped (Build Docs, Console Pin Gate, Packed-tarball smoke, and the edit-event duplicates of Auto Label and Check PR Size). Concluded success: Check Changeset (both), Governed Surface Queue Guard, Spec property liveness, Type Check · source gates and · debt ledger, Build Core, Dogfood Regression Gate 1/3, Dogfood Verify CLI, Check Documentation Links, Flag docs affected by code changes, filter, and the four PR-shape guards (both events). In progress, recorded as in progress: Lint & Repo Gates (where the ADR-0087 registration, migration-registry and generated-artifact gates run), Type Check · consumer gates and · workspace, Test Core 1/6–6/6, Dogfood Regression Gate 2/3 and 3/3, Temporal Conformance. The seat reads them green before queueing; at 7bb7b3a the same families concluded green on the code this round does not change.

Implemented-by: claude/issue-20437-replica-needs-syncurl
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

This was referenced Sep 28, 2026
…t a tracker number

The reason of turso-config-forced-replica-without-sync-url-refused, the
text os migrate meta prints under why:, opened with a tracker number.
It now says what was decided and why: a replica is defined by its
remote, the ruling weighed refusing the shape against documenting a
replica with no remote as a local mode, and refused it, because with
no remote there is no replica mode to document. The id, surface,
replacement, acceptanceCriteria and the ADR-0087 registration are
unchanged. registry.ts regenerated by gen:migration-registry.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 519cfbce5fd4ea5de7eca841604225d389f15bb2
Local-runs: none

Delta review, read-only, at tier, on PR #20504 for card #20437, branch claude/issue-20437-replica-needs-syncurl, after the PASS 5877910448 at 7bb7b3a and the delta PASS 5878149990 at 5dfa45e. Judged here: only what moved since 5dfa45e, plus the head's CI. Inputs: the card body and all seven comments (triage 5871347046, claim 5876481971, the three os-dev-reports 5877637196 / 5878055796 / 5883746538, the hold 5878163777 and the maintainer's ruling 5883364572), the PR body and file list, the net diff against main at the new merge base 1c761c0, and the check-runs on the head (read once, last). Sources were read with a read-only git fetch, git show, git diff, git log, git grep, git ls-tree and a git merge-tree --write-tree replay in the shared checkout; no worktree, checkout, build, test, gate or ablation was run. Nothing was posted. The cloud producer census is waived by the maintainer (5883364572, 「20504 不考虑 cloud 现有数据」) and is not judged.

① Derived judgments

1. The merge 04be827ce — clean, and no hunk of either side is lost. Right. Parents are 5dfa45e and main's 1c761c0 (a true merge, first-parent chain intact, so no rebase and no force-push). A read-only git merge-tree --write-tree 5dfa45e9f 1c761c0d7 answers tree 7713b3189bd9f6ad1a9e7c6856696d6efd6b46e9 with no conflict, and that is the merge commit's own tree byte for byte: the merge is exactly the textual replay of its parents, with no hand edit. Main moved 464 files in the window 4a1df19..1c761c0; exactly two of them are PR paths, packages/drivers/driver-turso/src/turso-driver.ts and packages/spec/src/migrations/registry.ts. For each of those two files the three-way check holds with hunk headers stripped: the diff 5dfa45e..04be827 equals main's own diff 4a1df19..1c761c0 line for line (main's side fully carried), and the diff 1c761c0..04be827 equals the PR's own diff 4a1df19..5dfa45e line for line (the PR's side fully carried). At the head turso-driver.ts holds REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL (the constant at :1138, thrown at :1601) beside main's setDeclaredValueShapeResolver wiring (:1644) and case '$empty': (:2603); main's hunk is #20444's eleven lines (the value-shape resolver handed to the remote transport, and $empty added to the presence-flag cases beside $null / $exists), which touch the filter face and not the constructor's config path. Neither copy of turso.zod.ts moved on main in the window (main moved other data/driver files: common, config-registry, memory, mongo, mysql, pg-url-grammar, postgres and their tests, none turso). Both turso.zod.ts copies, the four driver test files, turso.test.ts, the README and the changeset are byte-identical between 5dfa45e and the head.

2. packages/spec/src/migrations/registry.ts is current for the merged tree, and this PR's entry is registered once. Right. Read against the generator's rules in packages/spec/scripts/build-migration-registry.ts (the entry file's unbroken leading // run plus the object literal, each line indented four spaces, the trailing ; replaced by ,; entries sorted by major, then by id in codepoint order): the entry at the head projects to a 47-line block that occurs exactly once in the registry, between translation-per-app-settings-platform-only and turso-config-timeout-unit-in-key; the id turso-config-forced-replica-without-sync-url-refused occurs once (:16338); the registry holds 315 semantic ids, all unique, against 315 files under entries/semantic/ (main added four in the window: qa-scenario-requires-plugins-retired, rest-api-documentation-version-retired, stack-config-default-export-unbuilt-refused, view-filter-rule-operator-input-canonical, so 311 → 315), and both step blocks (step17: 77 ids, step18: 238 ids) are in the generator's id order. The entries/retired-keys/ and entries/retired-defs/ directories hold 232 and 206 files, the counts the report cites. The round's registry hunk is the entry's reason hunk re-indented, +4 / −1 in each file, and nothing else in the file moved. Reading only; CI's Lint & Repo Gates, where check:migration-registry and check:generated run, concluded success on this head.

3. The PR's net diff against the new base equals its net diff at 5dfa45e against 4a1df19, the reword aside. Right. With index and @@ lines stripped, git diff 4a1df1965 5dfa45e9f and git diff 1c761c0d7 519cfbce5 differ in 14 lines, all inside two hunks: the entry's reason opening and the registry's copy of it. The stats move from 12 files +533 / −43 to 12 files +539 / −43, the +3 net lines in each of the two files being the +4 / −1 reword. No other hunk differs.

4. The reword of 18.turso-config-forced-replica-without-sync-url-refused.ts — no tracker number in any author-shown field, the wording TRUE, the identity unchanged, nothing pinned the old text. Right. (a) Tracker numbers: with the string literals joined and scanned for # followed by 4 or 5 digits (the CLI pin's TRACKER_ID, /#\d{4,5}\b/), surface, replacement, reason and acceptanceCriteria each read 0; the whole file reads 0 at the head and 1 (#20437) at 5dfa45e, the lit control the report names. (b) Truth: the new opening says a replica is defined by its remote; that the ruling of 2026-09-28 weighed refusing the shape against documenting a replica with no remote as a local mode, and refused it; and that with no remote there is no replica mode to document, only a declaration nothing honours. Each clause is triage 5871347046, dated 2026-09-28T13:55Z: the card body posed exactly those two roads ("Or, if triage reads a replica without a remote as legitimate, it becomes a documented local mode instead"), and triage answered "Direction: refuse. A replica with no remote is not a mode", with "An embedded replica is defined by its remote, so there is no legitimate 'local replica' to document instead" and "A declared mode the runtime ignores is the ADR-0049 shape (declared ⇒ enforced)". The shape is form D as the two entries that define it state it ("no tracker number anywhere in the author-shown text; the decision is stated in words") and as 24 sibling entries at the head state their rulings (for example 17.datasource-config-inline-credential-refused: "The maintainer ruled on 2026-08-12 …"). The rest of reason is byte-identical to 5dfa45e. (c) Identity: the id, surface, replacement and acceptanceCriteria are byte-identical; the registration is once (judgment 2); the changeset's adr-0087 marker still reads registered turso-config-forced-replica-without-sync-url-refused, byte-identical. (d) Old text: 20437. An embedded replica has zero hits at the head; the id appears only in the changeset, the entry and the registry; the phrase "An embedded replica is a local file kept in sync" otherwise occurs in the two schema messages, the driver's header, replica-file.testkit.ts and the changeset, none of them an assertion on the entry. The CLI pin packages/cli/test/migrate-meta-engine-guidance.test.ts selects by COVERED_PREFIXES, which holds no turso- prefix at the head. os migrate meta prints reason under why: (packages/cli/src/commands/migrate/meta.ts:524), so the reworded field is the author-shown one. Observed and not a defect: #20437 remains in code comments and in one parity-test title; those are developer-facing, were present at 5dfa45e, and are outside the ruling's letter (text an author is shown).

5. PR body, "Patch round 2 (merge + form D)", sentence by sentence. Intro: follows 5883364572; cloud not measured, waived, not read or edited — TRUE (waived; not judged). "The round changes no code, test, schema text, refusal message or changeset" — TRUE (the round's two commits touch the entry and the registry only; every other PR file is byte-identical to 5dfa45e). "The changeset's level and the Clause-② line do not move" — TRUE. Item 1: the merge sentence with its parents — TRUE; "by bash scripts/pm/os-regen-merge.sh" — dev-stated, consistent with the default merge subject and a tree equal to the textual replay; "no rebase and no force-push" — TRUE; "main had moved over two of this PR's files, and both auto-merged with no conflict" — TRUE (exactly two; merge-tree equality); the registry.ts bullet (stages 4–6 restated other entries, new entries landed) — TRUE (#20509, #20522, #20536 are in the window; six entries added, four semantic); the turso-driver.ts bullet (the $empty resolver wiring and presence-flag case, both sides present beside the refusal) — TRUE; "packages/spec/src/data/driver/turso.zod.ts did not move on main in that window" — TRUE; "The merge left no os-regen deferral" — TRUE by construction (the merge tree is the textual replay; no marker, no repair commit; the branch's only commits since 5dfa45e are the merge and the reword). Item 2: check:migration-registry current at 315 / 232 / 206 — dev-run, and the three counts are TRUE against the entry directories; "gen:migration-registry then wrote no change" — dev-run, consistent with the registry reading as the generator's projection; "its registry.ts hunk is the entry's hunk re-indented (+4 / −1 in each file)" — TRUE; "spec-changes.json and docs/protocol-upgrade-guide.md still project no major-18 entry" — TRUE (zero hits for all six major-18 ids in both files); check:spec-changes / check:upgrade-guide / check:generated green — dev-run, Lint & Repo Gates success on the head; "Against the new merge base the net diff is 12 files, +539 / −43" — TRUE. Item 3: the file and "reason is the why: line os migrate meta prints" — TRUE; "id, surface, replacement, acceptanceCriteria, the registration and the ADR-0087 disposition are unchanged. Only the opening moves" — TRUE; the Before and After quotes — TRUE against the two blobs; "The rest of reason is byte-identical" — TRUE; "The date and the lesson are triage's ruling (5871347046)" — TRUE; the four-field census reading 0 now and 1 at 5dfa45e — TRUE, re-read here; "No test asserted the old sentence" — TRUE; the CLI pin selects by id prefix and turso- is not covered — TRUE at the head (the "after stage 7 on main" half concerns f11b5f2, outside this review's inputs, not judged); "No other test quotes the text" — TRUE; the sibling "still opens with a tracker number, and it is untouched" — TRUE (#19977.; no diff 5dfa45e..head). Readings at 519cfbc: dev-run; CI is the gate. Consistency: "78 files" against round 0's 77 — main added turso-20444-empty-operator.test.ts under driver-turso in the window; "12 paths vs merge base 1c761c0" — TRUE; "check:doc-authoring does not read a migration entry's prose fields … the proof for item 3 is the field census" — consistent with the CLI pin being the only prose gate and its prefixes excluding turso-. "Main moved after the merge … f11b5f2 … merges cleanly … registry current" — outside the inputs, not judged; the PR read at review time answers mergeable: true, mergeable_state: clean against current main, which agrees. "The branch was not merged again this round" — TRUE. Deviations: two pushes — consistent with the two commits; "The merge commit carries no trailer" — TRUE (empty body); "The reword commit ends with the model-free trailer pair" — TRUE (Co-authored-by: Claude and Claude-Session). Sentences elsewhere in the body the merge could have made stale: the H5 and Gates readings of "311 semantic" are scoped to 7bb7b3a by "Every final reading below was taken at head 7bb7b3a unless it says otherwise", and this section says otherwise (315); the stored-row line references (datasource-connection-service.ts:355 / :457 failed-degraded, datasource-admin-plugin.ts:707 "Failed to build driver") still hold at the head; the opening paragraph's one named merge (9bf5e67) is incomplete now, not false, and each round's section names its own merge. None found stale.

6. The in-repo producer census still holds at the merged head, so the changeset's blast-radius sentence stays TRUE after 464 files moved. mode: 'replica' outside tests, the two schema copies, the driver, the changeset and the migrations occurs only in the driver README's prose; no process.env name under packages/*/src maps to a sync url or a transport mode; buildTursoDriverConfig still reads an authored config.mode (turso-driver-config.ts:205).

② Semver level

Unchanged and still right after the merge: @objectstack/spec and @objectstack/driver-turso minor, BREAKING (the ! in the title, the bold BREAKING in the body, the (narrowing) arm), Clause-②: yes (narrowing). The changeset is byte-identical to 5dfa45e and 7bb7b3a, and its claim still matches what the diff publishes: main moved neither turso.zod.ts copy nor the constructor's config path in the window (its turso-driver.ts hunk is #20444's $empty filter face), so the accept-set change is exactly the one narrowing the changeset states, and this round moves only published registry data text (a D3 entry's reason), adding no export, key, describe or JSON-Schema change. The adr-0087 marker registered turso-config-forced-replica-without-sync-url-refused names an id that is new in this diff and registered once (judgment 2). The PR body's Clause-② line is the claim's (5876481971) and the changeset's. skip-changeset does not apply. Clause-②: yes (narrowing) — right.

③ Boundary flags

open_questions: none declared in 5883746538; none found.

Round-2 deviations, each answered: (1) two pushes where the order said one — process only, both commits on the PR branch, no new PR, no contract effect. (2) Main moved after the merge and the branch was not merged again — outside this review's inputs; the PR reads mergeable: clean at review time, and the landing queue re-tests against main. (3) Budgeted pr_create and label-write not spent — nothing to judge. (4) Local log housekeeping — nothing to judge; CI is the gate for this head. (5) The merge commit carries no trailer and the reword commit carries the model-free pair — as in rounds 0 and 1, no breach.

The cloud producer census: waived by the maintainer in 5883364572; not judged here, per the ruling. The round-0 and round-1 flags carry as answered in 5877910448 and 5878149990; the round-1 text follow-up (the entry's surface mirror clause) stayed closed, and the ruling's own follow-up (the tracker number in the entry's reason) is closed by this round (judgment 4).

Out-of-scope notes carried, not this PR's by order: (a) the sibling entry turso-config-transport-mismatch-refused still opens its reason with #19977. and carries the mirror overstatement for its forced-mode shapes; no gate holds the turso- family's prose (the CLI pin's prefixes exclude it) — carrier #20233 / the seat, as the report says. (b) A forced mode: 'local' beside syncUrl replicates although declared local — carried from round 0, carrier the seat, recommended for filing bare. (c) Observed here: #20437 in driver code comments and one parity-test title — developer-facing text, not author-shown, pre-existing; a nit for whenever those files are next touched.

Check-runs on the head, read once at the end of this review (2026-09-29T04:46:56Z): 42 check-runs on 519cfbc — 37 success, 5 skipped, 0 in progress, 0 failing. Two events ran (the push at 04:04Z and the PR-body edit at 04:36Z), so Check Changeset, Auto Label, Check PR Size and the four PR-shape guards appear twice. Skipped: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), and the edit-event duplicates of Auto Label and Check PR Size — path- or event-skips by roster. Success: Lint & Repo Gates, Check Changeset (both events), Build Core, Test Core 1/6–6/6 and its rollup, Type Check · source gates / consumer gates / debt ledger / workspace, TypeScript Type Check, Dogfood Regression Gate 1/3–3/3 and its rollup, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Spec property liveness, Governed Surface Queue Guard, Check Documentation Links, Flag docs affected by code changes, filter, Auto Label and Check PR Size (push event), and the four PR-shape guards on both events. Every run is concluded and green or skipped by roster.

Implemented-by: claude/issue-20437-replica-needs-syncurl
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 04:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit c876a74 Sep 29, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20437-replica-needs-syncurl branch September 29, 2026 05:18
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…its that decided them (stage 3) (objectstack-ai#20533)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 3 of the staged sweep. It covers
`packages/spec/src/data/**` and nothing else. It leaves out the files an
open PR or an in-flight claim holds: `data-engine.zod.ts`,
`data-engine.test.ts`, `hook.form.ts`, `analytics*.ts`,
`cube-member-inner-name-retirement.test.ts`, `driver/turso.zod.ts` and
`filter-subtree-provenance.ts`, as the claim names them. It also leaves
out four files that open PRs started editing after the claim:
`driver/turso.test.ts` (PR objectstack-ai#20504, objectstack-ai#20437's, opened 2026-09-28T20:08Z),
`object.form.ts` (PR objectstack-ai#20519, objectstack-ai#20432's, 21:55Z), `object.zod.ts` (PR
objectstack-ai#20521, objectstack-ai#20494's, 22:10Z) and `filter-logic-conformance.ts` (PR objectstack-ai#20523,
objectstack-ai#20444's, 22:39Z). See Acceptance notes. Later stages cover the other
areas, so this PR says `Part of`.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **163 sites on 161 lines in 44 files,
covering 40 numbers**. Each rewritten line now cites the commit in
`origin/main` history that decided what the line describes, and it says
in its own words what that commit decided.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 43 dead numbers in scope.
ADR-0104 names objectstack-ai#12380 only as a reference, and ADR-0055 states the rule
that objectstack-ai#8772's ruling enforced, not the ruling itself. So every anchor is
a commit: **38 distinct shas**. One number was dropped rather than
anchored: objectstack-ai#17286, a tracking card that recorded an axis as undecided,
under which no commit landed. The sentence keeps its reason in words.

Three comment sites in scope are left on purpose (see Acceptance notes).
Two are the `[objectstack-ai#6259]` marker in `api-derivation.ts:163`, which a test
string reads, and the test comment that names that marker. The third is
`field.zod.ts:370`, whose `objectstack-ai#6111` is objectui's number.

Only comments changed. Every source file keeps its line count (174 lines
out, 174 in, over 45 files), so no line citation into these files moves.
Thirteen of those 174 lines held no dead citation. Eleven are the other
half of a sentence that had to be reflowed or rewritten. One is a table
header (`value-roundtrip-conformance.ts:20`, 「card」 to 「card or commit」,
because its row now holds a commit). One is `api-derivation.ts:164`,
which now carries the `[objectstack-ai#6259]` sentence's commit. No code token moves
(see the guard below). The 41 string-literal sites that carry a dead
number are tokens, so they are left as they were and listed below.

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. No PR number stands on an added
line.

Two more kinds of file change, both mechanical:
- **One regenerated reference page.** Two of the rewritten docblock
lines (`feed.zod.ts:15`, `:18`) project into
`content/docs/references/data/feed.mdx`. `check:docs` proved that page
stale, and `pnpm --filter @objectstack/spec check:generated --fix`
regenerated only it. The diff is two lines, each the same substitution
as its source line. No page a held file projects into (`analytics.mdx`,
`data-engine.mdx`, `hook.mdx`, `driver-turso.mdx`) moved.
- **A `patch` changeset** for `@objectstack/spec` (see Changeset below).

## Census: `data/`, before and after

**Instrument.** This is the instrument of stages 1 and 2. It sends REST
`GET /repos/objectstack-ai/objectstack/issues/N` without following
redirects, for every distinct number cited in `packages/spec/src/data`.
The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`,
`objectstack-ai/objectstack`, `framework`, `pre-` or `post-`;
- widened here to the capitalised spellings of those qualifiers (`Pre-`,
`POST-`, `Framework`: 7 sites, one of them dead), which stage 2's
case-sensitive set did not read;
- N of 100 or more, excluding `summon` heads.

Each site is classified by the TypeScript parser as a line comment, a
docblock, a block comment or a string.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end. They read 24 of 24
lit (200) and 24 of 24 dead (404) over 8 checkpoints in both runs.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites, all of `data/` | in scope | excluded (held files) | in-scope
lines | in-scope files | dead numbers in scope |
|---|---|---|---|---|---|---|---|---|---|---|---|
| before | base `9bf5e67af`, probed 2026-09-28T19:32Z to 19:36Z | 618 |
571 | 47 | 0 | **240** | 207 | 33 | 204 | 47 | 43 |
| after | head `96fd49caa2`, probed 2026-09-28T23:19Z to 23:23Z | 600 |
571 | 29 | 0 | **77** | 44 | 33 | 43 | 16 | 21 |

**Before, in scope, by class.** 92 non-test docblock sites and 13
non-test line comments. 16 test docblock sites and 45 test line
comments. 39 test string sites. 2 non-test string sites.

**After, in scope.** 41 string sites and 3 comment sites remain, all
three deliberate. The head probe found no number newly dead since the
base probe: the same 571 numbers answer 200.

PR objectstack-ai#20226's area table read `data` 239 at an earlier base; this census
reads 240 at `9bf5e67af`. The 33 excluded sites sit in `object.zod.ts`
(15), `analytics.zod.ts` (3), `analytics-strictness-batchd.test.ts` (2),
`analytics-date-range-two-bound-window.test.ts` (1),
`driver/turso.zod.ts` (2), `driver/turso.test.ts` (3),
`filter-subtree-provenance.ts` (3), `filter-logic-conformance.ts` (3)
and `object.form.ts` (1). `data-engine.*` and `hook.form.ts` carry none.

## Per-number table

The counts are in-scope sites and files at the base. `rewritten / left`
gives comment sites rewritten and sites left. Every anchor was read in
its diff or message, not only in its subject: it is the commit that made
the change the line now describes, and its own diff or message names the
number it replaces.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#6111` (objectui) | 1/1 | 0/1 | objectui's number, left: see
Acceptance notes |
| `objectstack-ai#6259` | 5/2 | 1/4 | `6968885ef`: retires the producer-less `batch:
'bulk'` row of `DATA_ACTION_TO_API_OPERATION` and the prose calling
`batch` a runtime action. The marker and 2 test strings stay (see
Acceptance notes) |
| `objectstack-ai#6345` | 18/5 | 17/1 | `e2798fab7`: one driver vocabulary; both boot
hosts read the shared table; `mongo` to `mongodb`; turso a builtin; the
fork-1 and fork-2 refusals |
| `objectstack-ai#6571` | 10/2 | 8/2 | `2f3e79351`: `$between` endpoints accept the
ISO/clock strings the platform produces, as a bare string (rider ①) |
| `objectstack-ai#8495` | 9/2 | 6/3 | `4bfe1a539`: refuses `${…}` placeholders in
memory `persistence.path` / `persistence.key` at publish |
| `objectstack-ai#8656` | 1/1 | 0/1 | a test title only |
| `objectstack-ai#8696` | 20/8 | 17/3 | `90a12fb18`, the card's mongodb arm: a bound
secret rides beside an unmodified url as MongoClient `auth`. Its own
pins carry the multi-host form `new URL()` cannot parse and the bound
secret outranking `options.auth` |
| `objectstack-ai#8772` | 3/2 | 3/0 | `75b7c240a`: Direction 2 of the 2026-08-16
maintainer ruling. The builder forces `required: true` on a
`master_detail` under `controlled_by_parent`, and raw parse stays
tolerant. ADR-0055 stays cited beside it |
| `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2`: stamp-only
`tenancy.organizationField`, declared by `sys_api_key` |
| `objectstack-ai#8794` | 2/1 | 2/0 | `1850ebbb0`: corrects the reuse-safety claim on
the filter-subtree mark from the survey's measurement, and routes a
mechanism change to a spec-seat ruling (stage 1's anchor too) |
| `objectstack-ai#8836` | 2/1 | 2/0 | `1850ebbb0`: the same commit, which pins the
invariant (one line carries both numbers) |
| `objectstack-ai#8873` | 6/3 | 6/0 | `096106522`: a bound `credentialsRef` reaches
the postgres server on the DSN branch. Its diff records that `pg` sends
a password only when the server asks |
| `objectstack-ai#8874` | 1/1 | 1/0 | `d70428ae7`: a declared mysql `ssl` reaches
`mysql2` as its own TLS options object, because `mysql2` rejects a bare
boolean |
| `objectstack-ai#8876` | 9/5 | 6/3 | `d634e665b`: exports `urlUserinfoUsername`, and
its diff states the asymmetry that a username is not credential material
|
| `objectstack-ai#9040` | 20/6 | 14/6 | `24206416a`: refuses a credential in the mongo
options passthrough at publish, and redacts the passthrough secret paths
on read |
| `objectstack-ai#9041` | 22/2 | 17/5 | `d491625c1`: refuses a bound `credentialsRef`
with a user-less mongo `config.url`, with the triage's fences |
| `objectstack-ai#10165` | 5/1 | 1/4 | `801296050`: `ttl.onlyWhen` with the canonical
null predicate (maintainer ruling 2026-08-20, option A) |
| `objectstack-ai#10274` | 1/1 | 1/0 | `d1ba685ec`: re-measures the objectui pin
citations and gates the class |
| `objectstack-ai#10329` | 6/2 | 6/0 | `15d58dbf1`: retires the import lookup
transform's steering params (ADR-0049) |
| `objectstack-ai#10347` | 2/1 | 2/0 | `530c1df65`: the Archiver honours a declared
`ttl` (maintainer ruling 2026-08-20) |
| `objectstack-ai#10527` | 2/1 | 1/1 | `5649efbf9`: refuses a diverging retention +
ttl + archive triple at parse time |
| `objectstack-ai#11065` | 7/3 | 5/2 | `20950404c`: a boolean aggregand counts as 1 or
0 in `avg` and `sum`, the first face aligned. No commit message names
the card; this is where the number first entered the tree |
| `objectstack-ai#11195` | 3/1 | 2/1 | `b37231883`: `UserActionsConfigSchema` adopts
`group` / `hideFields` / `rowColor` |
| `objectstack-ai#11215` | 1/1 | 1/0 | `42a117b88`: documents
`NoSQLIndexSchema.unique`'s deliberate scope-vocabulary omission |
| `objectstack-ai#11350` | 1/1 | 1/0 | `ece4dad31`: records the 2026-08-23 maintainer
ruling on entry nameability (stage 1's anchor too) |
| `objectstack-ai#11408` | 2/1 | 1/1 | `f11fc61c5`: declares `editMode` (maintainer
ruling 2026-08-24) |
| `objectstack-ai#11507` | 5/2 | 5/0 | `88b9d749a`: declares `sys_activity.type` an
open, author-extensible vocabulary (maintainer ruling 2026-08-24,
direction 4) |
| `objectstack-ai#11658` | 1/1 | 1/0 | `1a6a19c31`: opens `RecordActivityProps.types`
to author-contributed kinds |
| `objectstack-ai#12380` | 4/2 | 4/0 | `4045b954d`: makes the SQLite `Field.json`
codec injective; its message carries the measured boundary |
| `objectstack-ai#12868` | 1/1 | 0/1 | a test title only. Its comment site sits in
`object.form.ts`, now held by PR objectstack-ai#20519; its deciding commit is
`c459da6bc` (see Acceptance notes) |
| `objectstack-ai#13156` | 1/1 | 1/0 | `fd289be45`: strips tracker ids from
function-declaration-built refusal prose (the card's A half) |
| `objectstack-ai#13644` | 3/2 | 2/1 | `34ce8e7db`: declares
`ctx.referentialFieldClear` on `HookContextSchema` |
| `objectstack-ai#14426` | 2/2 | 1/1 | `40a44b91b`: the undefined-comparand refusal
prescribes the null predicate by its ruled spellings, position-safe |
| `objectstack-ai#14676` | 1/1 | 1/0 | `13c48c2a5`: retires `connector.errorMapping`;
its test states the same assertion-set reasoning |
| `objectstack-ai#16126` | 2/2 | 2/0 | `859ded3ec`: refuses a whitespace-only
`reference` on lookup / master_detail |
| `objectstack-ai#16685` | 4/2 | 4/0 | `ed7243d52`: accepts boolean / toggle for sum /
avg / min / max (decision batch objectstack-ai#80) |
| `objectstack-ai#16867` | 3/2 | 2/1 | `0ee32edef`: `notNull` / `not_null` prescribe
`storage.notNull`, not `required` |
| `objectstack-ai#17014` | 3/2 | 2/1 | `80aef8032`: the one-day date-range presets
prescribe a one-day window, and the table states its end-token
convention |
| `objectstack-ai#17286` | 1/1 | 1/0 | dropped: a tracking card with no landing. The
sentence now says the card is gone and to measure `driver-memory` for
the open set |
| `objectstack-ai#17348` | 1/1 | 1/0 | `51efbf116`: pins the `driver-memory` temporal
text-operator divergence by name in that driver's conformance suite |
| `objectstack-ai#17590` | 1/1 | 1/0 | `e04a0aff2`: `$contains` on a JSON column is a
per-dialect membership test (director-seat ruling 2026-09-12) |
| `objectstack-ai#18012` | 8/3 | 7/1 | `176b03582`: `$between` requires two non-blank
endpoints (decision batch objectstack-ai#146 item 5, letter A) |
| `objectstack-ai#19377` | 6/2 | 6/0 | `a60c913de`: refuses a `{ $field }` reference
as a `$between` endpoint at the runtime filter door |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1), and every one is an ancestor of the base
(`merge-base --is-ancestor`, exit 0). That is 38 distinct shas.

Wordings to check, each true of its commit:
- `datasource.zod.ts:352` names only the card's mongo arm (`90a12fb18`)
for "the defect class … closed", because the paragraph is about mongo.
The card's mysql arm (`72050cc47`) is not cited anywhere in this stage.
- `datasource.zod.ts:354`: 「the triage's, as commit d491625 landed
them」. `d491625c1`'s message lists the fences as "per triage".
- `filter.zod.ts:1021-1025`: the `objectstack-ai#17286` pointer becomes 「was measured
on a tracking card … That card is gone: measure `driver-memory` for the
open set, ⛔ not this text.」 The warning that this paragraph is not the
authority is kept.

## The 41 string sites left as tokens

- **Test titles and test-code strings (39 sites).**
`driver/driver-credential-refusal.test.ts` 14, `object.test.ts` 6,
`datasource-credential-redaction.test.ts` 3,
`driver/driver-placeholder-refusal.test.ts` 3, `filter.test.ts` 3,
`api-derivation.test.ts` 2 (the `split('[objectstack-ai#6259]')` literal and its
message), `field.test.ts` 2, and 1 each in `date-range-presets.test.ts`,
`driver/postgres.test.ts`, `field-rows-option-description.test.ts`,
`filter-comparand-type.test.ts`, `hook.test.ts` and
`object-strictness-batch20.test.ts`.
- **Non-test strings (2 sites).** `aggregation-conformance.ts:398` and
`:407`, the `note` of two exported `AGGREGATION_CASES` rows (`objectstack-ai#11065`,
`objectstack-ai#11151`). They ship as data. Their only readers are driver conformance
suites, which print a `note` as the assertion message when a case fails,
to a driver developer and never to a metadata author. So they are
neither comments nor form D author-shown text. This is the same
disposition stage 1 gave the two `why` strings and stage 2 the
`PROVENANCE_WAIVERS` reason.

No author-shown text in `data/` carries a dead number, so nothing here
is objectstack-ai#20233's form D.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `9bf5e67af`
against head `96fd49caa2`. It uses the TypeScript parser's leaf tokens,
so template literals are scanned in context, and it excludes JSDoc
nodes. It ran over all 45 touched `.ts` files.

- Real run: 140,379 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control: 0 files changed, as expected (exit 0).
- Positive control (a declaration inserted into `feed.zod.ts`): 1 file
reads DIFFER (exit 1).
- Positive control (one digit changed inside the `split('[objectstack-ai#6259]')`
string in `api-derivation.test.ts`): 1 file reads DIFFER (exit 1).

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.

Measured on the built package: 14 of the touched sources are
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
docblocks also reach `dist`. `88b9d749a`, `e2798fab7` and `24206416a`
each appear in 1 declaration file. `24206416a` appears in 20 bundled
`.js` files and `2f3e79351` in 28. The positive control, a pre-existing
`feed.zod.ts` docblock sentence, appears in `dist/data/index.d.ts`.

## Gates (head `96fd49caa2`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs` exits
0. The self-test passes 73 cases in 7 batteries. The live run judged 11
citations across 25 files, and all 11 resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at the final head derived 108
families, and all 108 exit 0. `--ran` reports 108 run, 0 NOT MEASURED, 0
unrun, and exits 0. (`check:i18n` was derived at the earlier heads from
`object.form.ts`, and left the set when that file went back to base.)
- At an earlier head, four gates first exited 3 (PREREQUISITE NOT MET)
because the workspace was unbuilt: `check:doc-formula-expressions`,
`check:doc-security-posture`, `check:skill-examples` and
`check:docs-transcript-drift`. At the final head a full `turbo run
build` of `./packages/*` ran first (71 tasks, exit 0, under the shared
verify lock), and every gate exited 0 on its first run.
- `check:generated` was run under the lock against that build: all 15
artifacts are up to date.
- **Build, tests, typecheck and lint:**
  - `pnpm --filter @objectstack/spec build` exits 0.
- `vitest run --maxWorkers=2 src/data` in `packages/spec` at the final
head: 107 files and 3,517 tests pass (1 todo), covering every touched
test file.
- The 12 spec suites outside `src/data` that read `data/` source text
pass at the final head: 12 files, 503 tests. These are
`scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`,
`src/system/constants/platform-object-names.test.ts`,
`src/type-alias-convention.pin.test.ts` and `src/ui/dashboard.test.ts`.
- `pnpm --filter @objectstack/spec typecheck` at the final head exits 0,
including `check:test-typecheck` (53 files, 251 errors, 138 pinned
signatures held).
- Lint, as a proven narrowing at the final head: `eslint
--no-inline-config --format json` over the 45 touched `.ts` files gives
45 files, 0 errors and 0 warnings. All 45 are in eslint's own population
(`isPathIgnored` is false for each). `eslint.config.mjs` never enables
type-aware linting (no `parserOptions.project`, which its own line 328
states), so a comment edit here cannot move the verdict on any untouched
file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **The `[objectstack-ai#6259]` marker.** `api-derivation.test.ts:236` splits
`DATA_ACTION_TO_API_OPERATION`'s TSDoc on the literal `[objectstack-ai#6259]`, and a
test string may not change here. So the marker line
`api-derivation.ts:163` is byte-identical to the base, and the test
comment at `:232` that names the marker stays too. The sentence's
deciding commit sits on the next line instead: 「(both by commit
6968885)」. A first attempt wrote the commit onto the marker line
itself. The diff-scoped `check-issue-citations` then read the kept
`objectstack-ai#6259` as an added citation and exited 1, so it was moved one line down
(commit `b93f08f8d0`).
- **objectui's `objectstack-ai#6111`.** `field.zod.ts:370` reads 「objectui#6110 +
objectstack-ai#6111 (section)」. The qualifier covers only the first number, so the
citation grammar reads `objectstack-ai#6111` as this repository's (404 here). It is
objectui's number: its introducing commit `f887e5249` writes
`(objectui#6111)` in the same diff, and `objectstack-ai/objectui`
answers REST 200 for objectstack-ai#6111 to this session (and for objectstack-ai#6110 and objectstack-ai#10264).
objectui has no `refs/pull/6111/head`, so it is an issue there, not a
PR. The line is left unchanged. This is objectstack-ai#20330's grammar family, the
same as stage 2's `objectui PR objectstack-ai#10264`, and it is noted there, not
filed.
- **Capitalised qualifiers.** `CITATION_RE` classes `Pre-#N`, `POST-#N`
and `Framework#N` (7 sites in `data/`) as cross-repo and never judges
them. This census read them as this repository's. One was dead and is
rewritten here (`object.test.ts:223`, `POST-objectstack-ai#10347`). This is the same
objectstack-ai#20330 family as stage 1's `pre-` / `post-` finding.
- **Four files held after the claim.** Each joined the exclusions and
went back to the base bytes (hypothesis 2 of the dispatch). Each PR's
hunks were disjoint from this PR's lines, but the dispatch's rule is
file-level.
- `driver/turso.test.ts`: PR objectstack-ai#20504 (objectstack-ai#20437's) opened at
2026-09-28T20:08Z and edits it. Its two comment sites (`:4`, `:58`, both
`objectstack-ai#6345`) went back to blob `7fe99ebf9` in commit `86463ed0a1`. A
no-driver `merge-tree` of that head with PR objectstack-ai#20504's head `5dfa45e9f`
exits 0.
- `object.form.ts`: PR objectstack-ai#20519 (objectstack-ai#20432's) opened at 21:55Z and edits it.
Its one comment site (`:256`, `objectstack-ai#12868`, whose deciding commit is
`c459da6bc`) went back to blob `60713e06f` in commit `3479600dda`.
- `object.zod.ts`: PR objectstack-ai#20521 (objectstack-ai#20494's) opened at 22:10Z and edits one
line at `:2123`. Its 15 comment sites (`objectstack-ai#8772`, `objectstack-ai#10165`, `objectstack-ai#10347`,
`objectstack-ai#10527`, `objectstack-ai#11195`, `objectstack-ai#11408`, `objectstack-ai#13608`) went back to blob `befde04ca` in
commit `96fd49caa2`. Their deciding commits are `75b7c240a`,
`801296050`, `530c1df65`, `5649efbf9`, `b37231883`, `f11fc61c5` and
`fc9ba76a5`, all read for this stage.
- `filter-logic-conformance.ts`: PR objectstack-ai#20523 (objectstack-ai#20444's) opened at 22:39Z.
Its 3 comment sites (`objectstack-ai#13195`) went back to blob `c9b32acba` in the same
commit. Their deciding commit is `9dac1ae01`, with `PR objectstack-ai#13529` as the
link.
- **What stays for later stages.**
- The 33 dead sites in the held files listed above. The later stage can
reuse the deciding commits named for them here.
  - The 41 string sites and the 3 deliberate comment sites above.
- The `data/` numbers that also appear in
`packages/spec/src/migrations/**`. Those are objectstack-ai#20233's form D, or the
migrations stage.
- **The rung.** Several anchored changes also have ADR-0087 entries in
`packages/spec/src/migrations`. Examples are
`cbp-master-detail-required-forced` for objectstack-ai#8772,
`filter-between-blank-endpoint-refused` for objectstack-ai#18012, the `datasource-*`
entries for objectstack-ai#9040, objectstack-ai#9041 and objectstack-ai#8873, and the
`mapping-lookup-params-removed` conversion for objectstack-ai#10329. This PR takes the
commit rung, as stages 1 and 2 did, so it is precedent-consistent. The
D3 id is the more durable in-repo record, if the ruling's first rung is
later read to include those entries.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`, so
20 of the 45 touched `.ts` files never enter its judging population. The
added-minus-removed count over the whole diff covers them: 0 numbers
added.
- **Base.** The branch is 22 commits behind `origin/main` (`1378ec7c0c`,
read at 2026-09-29T00:18Z). Four of those commits touch `data/`, all in
excluded files: objectstack-ai#20475's `hook.form.ts`, objectstack-ai#20487's `data-engine.*`, and,
since this stage excluded them, PR objectstack-ai#20521's `object.zod.ts`
(`9e1689f8e2`) and objectstack-ai#20444's `filter-logic-conformance.ts`
(`fb386074f5`). None touches a file in this diff, and a no-driver
`merge-tree` of the head onto `1378ec7c0c` exits 0. So there was no
merge. The open-PR file lists were re-read at 00:18Z: 11 open PRs, none
touching a file in this diff.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… commits that decided them (stage 4) (objectstack-ai#20548)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 4 of the staged sweep: the `data/` remainder. It covers
the six `packages/spec/src/data/` files stage 3 (PR objectstack-ai#20533, landed
`03b19d9cfd`) left out because an open PR held them, and nothing else.
They are `object.zod.ts`, `filter-logic-conformance.ts`,
`object.form.ts`, `data-engine.zod.ts`, `data-engine.test.ts` and
`hook.form.ts`. Later stages cover the other areas, so this PR says
`Part of`.

The census below measured all six. Three of them carry comment or
docblock sites that cite a tracker number answering 404.
`data-engine.zod.ts`, `data-engine.test.ts` and `hook.form.ts` carry
none, so they are not in the diff.

Every such site has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **19 sites on 19 lines in 3 files,
covering 9 numbers**. Each rewritten line now cites the commit in
`origin/main` history that decided what the line describes, and it says
in its own words what that commit decided. Where a PR number was already
on the line (`PR objectstack-ai#13529`), it stays beside the commit as the link.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 9 numbers: a search for each
number, with and without `#`, finds nothing there. So every anchor is a
commit: **9 distinct shas**. Stage 3 had already read these commits and
recorded them in PR objectstack-ai#20533's body. They were not copied from there. Each
one was re-read against the current line it anchors: its own message or
diff names the number it replaces, and it made the change the line
describes. `object.zod.ts` and `filter-logic-conformance.ts` moved on
`main` after stage 3 read them (PRs objectstack-ai#20521 and objectstack-ai#20523). Each site was
therefore re-read at this base, `03b19d9cfd`.

Only comments changed. Every source file keeps its line count (20 lines
out, 20 in, over 3 files), so no line citation into these files moves.
One of the 20 lines held no dead citation:
`filter-logic-conformance.ts:249`, the first half of a sentence reflowed
onto `:250`. No code token moves (see the guard below).

**No tracker number is added.** Every tracker number on an added line
was already in the hunk it replaces. `PR objectstack-ai#13529` stands on three added
lines, and on the three removed lines of the same hunks. It is the link
beside commit `9dac1ae01`, which stage 3 recorded the same way.

No reference page under `content/docs/references/` moved: none of the
rewritten docblocks projects into one (`check:docs` at the head: `226
generated files in sync`). The PR adds one `patch` changeset for
`@objectstack/spec` (see Changeset below).

## Census: the six files, before and after

**Instrument.** This is the instrument of stages 1 to 3. It sends REST
`GET /repos/objectstack-ai/objectstack/issues/N` without following
redirects, for every distinct number cited in `packages/spec/src/data`.
The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`,
`objectstack-ai/objectstack`, `framework`, `pre-` or `post-`;
- widened case-insensitively to `Pre-`, `POST-` and `Framework`, as in
stage 3;
- N of 100 or more, excluding `summon` heads.

Each site is classified by the TypeScript parser as a line comment, a
docblock, a block comment or a string.

Two cross-checks close the population. First, a raw `#N` count in each
of the six files equals the census rows plus the cross-repo rows in five
files. In the other two it is one higher, and the extra is a second
number after a slash inside a string (`objectstack-ai#5322/objectstack-ai#5134` in a `note`,
`objectstack-ai#6262/objectstack-ai#6433` in a test title). Both answer 200. Second, no spelled
citation (`issue N`, `PR N`, `card N`) occurs in any of the six.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end: 24 of 24 lit (200)
and 24 of 24 dead (404) over 8 checkpoints in the base run, and 21 of 21
lit and 21 of 21 dead over 7 checkpoints in the head run.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites, all of `data/` | dead sites, the six files | lines | files |
numbers |
|---|---|---|---|---|---|---|---|---|---|---|
| before | base `03b19d9cfd`, probed 2026-09-29T01:11:59Z to 01:15:49Z |
601 | 572 | 29 | 0 | **77** | 19 | 19 | 3 | 9 |
| after | head `53c9070dfd`, probed 2026-09-29T01:25:55Z to 01:29:35Z |
597 | 572 | 25 | 0 | **58** | 0 | 0 | 0 | 0 |

The head probe found no number newly dead since the base probe: the same
572 numbers answer 200. The base reading of 77 equals stage 3's after
reading at `96fd49caa2`.

**Per file.** Cited sites here are every in-repo citation the population
reads, live or dead.

| file | cited sites (base) | dead sites before | by class | dead sites
after |
|---|---|---|---|---|
| `object.zod.ts` | 120 | 15 | 8 docblock, 7 line comment | 0 |
| `filter-logic-conformance.ts` | 97 | 3 | 2 docblock, 1 line comment |
0 |
| `object.form.ts` | 31 | 1 | 1 line comment | 0 |
| `data-engine.zod.ts` | 48 | 0 | | 0 |
| `data-engine.test.ts` | 29 | 0 | | 0 |
| `hook.form.ts` | 0 | 0 | | 0 |

None of the 19 sites is a string, so this stage leaves no string token
behind.

## Per-number table

| number | sites / lines | anchor: what it decided |
|---|---|---|
| `objectstack-ai#8772` | 4 / 4, `object.zod.ts:2718`, `:2731`, `:2744`, `:2910` |
`75b7c240a`: Direction 2 of the 2026-08-16 maintainer ruling.
`ObjectSchema.create()` forces `required: true` on a `master_detail`
reference under `controlled_by_parent` and refuses an explicit
`required: false`. Raw parse stays tolerant, and runtime tolerance is
the ruling's other half. Its changeset records the measurement that only
the security gate closed that shape while the declaration surface
accepted it (`:2731`). ADR-0055 stays cited beside it. It is the same
anchor stage 3 gave `object.test.ts` |
| `objectstack-ai#10165` | 2 / 2, `object.zod.ts:818`, `:1036` | `801296050`:
`ttl.onlyWhen` with the canonical null predicate (maintainer ruling
2026-08-20, option A). One shared `onlyWhen` union, and both of
`retention.onlyWhen`'s conflicts mirrored. Its diff wrote both
`[objectstack-ai#10165]` blocks |
| `objectstack-ai#10347` | 3 / 3, `object.zod.ts:1006`, `:1042`, `:1049` |
`530c1df65`: the Archiver honours a declared `ttl`. It selects by the
ttl cutoff on `ttl.field` when `ttl` is declared, and by `created_at` /
`archive.after` otherwise (maintainer ruling 2026-08-20) |
| `objectstack-ai#10527` | 1 / 1, `object.zod.ts:1005` | `5649efbf9`: refuses a
diverging retention + ttl + archive triple at parse time. Its diff wrote
this very paragraph |
| `objectstack-ai#11195` | 1 / 1, `object.zod.ts:1791` | `b37231883`:
`UserActionsConfigSchema` adopts `group` / `hideFields` / `rowColor`
(the "last three" the line names) |
| `objectstack-ai#11408` | 1 / 1, `object.zod.ts:2189` | `f11fc61c5`: declares
`editMode` on the object document (maintainer ruling 2026-08-24, the
`objectstack-ai#10144` declare-or-rule-out family, which stays cited) |
| `objectstack-ai#13608` | 3 / 3, `object.zod.ts:2317`, `:2354`, `:2366` |
`fc9ba76a5`: `publicSharing.eligibility` is held at redemption, not only
at mint, fail-closed, with the undifferentiated `null` refusal. Its
changeset heads with objectstack-ai#13608. It is the same anchor stage 1 gave
`contracts/share-link-service.ts` |
| `objectstack-ai#13195` | 3 / 3, `filter-logic-conformance.ts:190`, `:250`, `:525` |
`9dac1ae01`, PR objectstack-ai#13529's squash commit, which stays as the link:
`$exists` means has-a-value on driver-memory's live mingo path, its
analytics face and driver-mongodb's `translateFilter` (the "last three
key-presence exits") |
| `objectstack-ai#12868` | 1 / 1, `object.form.ts:256` | `c459da6bc`: narrows the
per-option `default` key out of the form-view options vocabulary, which
offered a key nothing on that surface read. Commit `e808890958`, which
wrote this line, names objectstack-ai#12868 as the same offer-vs-door class |

The shas were checked at the base and again at `origin/main`
`288611e3e5`. Every one matches exactly one commit (`git rev-parse
--disambiguate`, count 1). Every one is an ancestor (`git merge-base
--is-ancestor`, exit 0 for 9 of 9). The control leg `e9584681a4` also
exits 0, and the repository is not shallow. For each commit, a grep of
its own message or diff finds the number it replaces. Seven of the nine
name it in the message. `fc9ba76a5` names it in its diff (20 lines,
including its changeset heading), and so does `c459da6bc` (8 lines,
including its changeset heading).

Wordings to check, each true of its commit:
- `object.zod.ts:2731` now reads 「closes that shape, and commit
75b7c24 records that the declaration and the enforcement disagree」.
The measurement was the card's. The commit's changeset records it: "only
the security gate closed that shape while the declaration surface
accepted it".
- `object.zod.ts:2189` reads 「Declared here by commit f11fc61's
maintainer ruling」, and `:2744` reads 「the other half of commit
75b7c24's ruling」. This is stage 3's wording for the same relation
(`object.test.ts`, 「the other half of commit 75b7c24's ruling」): the
commit that landed the ruling and quotes it.
- `object.zod.ts:1049` reads 「That is the whole of what [commit
530c1df] changed here」. Commit `52db1d1f2a` wrote the paragraph.
`530c1df65` is the change it describes.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `03b19d9cfd`
against head `53c9070dfd`. It uses the TypeScript parser's leaf tokens
(TypeScript from the head's lockfile), so template literals are scanned
in context, and it excludes JSDoc nodes. It ran over all 3 touched `.ts`
files. It is the stage-3 instrument, unchanged.

- Real run: 13,624 base tokens (object.zod.ts 8,774, object.form.ts
3,226, filter-logic-conformance.ts 1,624), **0 files with a token
change** (exit 0).
- Comment-insertion control (`object.form.ts`): 0 files changed, as
expected (exit 0).
- Positive control (a declaration inserted into `object.zod.ts`): 1 file
reads DIFFER at token 1629 (exit 1).
- Positive control (one digit changed inside the `objectstack-ai#5322/objectstack-ai#5134` `note`
string in `filter-logic-conformance.ts`): 1 file reads DIFFER at token
889 (exit 1).

Line balance: `object.zod.ts` +15 / -15, `filter-logic-conformance.ts`
+4 / -4, `object.form.ts` +1 / -1. Line counts are equal at base and
head: 3,240, 621 and 751.

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.
`Clause-②: no`: no export, key, value or type moves (the guard above).

Measured on the head's built package: `object.zod.ts` is
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
comments also reach `dist`:
- `9dac1ae01` appears in `dist/data/index.d.ts` (the
`filter-logic-conformance.ts` docblock) and in 4 bundled `.js` files;
- `fc9ba76a5`, `f11fc61c5` and `b37231883` each appear in 22 bundled
`.js` files, and `c459da6bc` in 12;
- the positive control, the pre-existing `object.zod.ts` sentence
「Fail-CLOSED at both points」, appears in 11 bundled `.js` files.

## Gates (head `53c9070dfd`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs` exits
0. The self-test passes 73 cases in 7 batteries. The live run judged 6
citations across 3 files: 3 resolve (`objectstack-ai#9138` twice, `objectstack-ai#11410`) and 3
resolve as a pull request (`objectstack-ai#13529`, the link).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at the head derived 79 families, and
all 79 exit 0. `--ran` reports 79 run, 0 NOT MEASURED, 0 unrun, and
exits 0. A full `turbo run build` of `./packages/*` ran first, under the
shared verify lock: 71 of 71 tasks, VERDICT command-exit 0. So no gate
met an unbuilt prerequisite.
- `pnpm --filter @objectstack/spec run check:generated`: under the lock
against that build, `All 15 generated artifacts are up to date`, VERDICT
command-exit 0.
- **Tests and typecheck:**
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/data` under the lock: Test Files 107 passed (107), Tests 3527
passed, 1 todo (3528), VERDICT command-exit 0. It covers every test in
`data/`, among them `object.test.ts`, which reads these schemas.
- The 13 spec suites outside `src/data` that read the touched files'
source text or pin their line numbers, under the lock: Test Files 13
passed (13), Tests 544 passed (544). They are stage 3's 12
(`scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`,
`src/system/constants/platform-object-names.test.ts`,
`src/type-alias-convention.pin.test.ts`, `src/ui/dashboard.test.ts`)
plus `src/shared/union-author-message-pins.test.ts`, which pins
`data/object.zod.ts:855`.
- `pnpm --filter @objectstack/spec typecheck` under the lock exits 0,
including `check:test-typecheck` (53 files, 251 errors, 138 pinned
signatures held).
- **Lint, as a proven narrowing at the head:** `eslint
--no-inline-config --format json` over the 3 touched `.ts` files gives 3
files, 0 errors and 0 warnings. All 3 are in eslint's own population
(`isPathIgnored` is false for each). `eslint.config.mjs` never enables
type-aware linting (no `parserOptions.project`, which its own line 328
states), so a comment edit here cannot move the verdict on any untouched
file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base.** The branch forked from `03b19d9cfd`, stage 3's landing.
`origin/main` then moved two commits (`05077d4c26`, PR objectstack-ai#20532, and
`288611e3e5`, PR objectstack-ai#20536), and neither touches `data/`. `dispatch-gates`
flagged its derivation as stale because `scripts/regen-artifacts.mjs`
had moved, so `origin/main` was merged in (`53c9070dfd`, a clean merge
with no driver-deferred path) before the gates ran. The PR's delta
against `origin/main` is exactly its 4 files. `origin/main` has since
moved two more commits: `7e36a3cd7c` (PR objectstack-ai#20531) and `ba5927f714` (PR
objectstack-ai#20460). Neither touches `data/` or anything the gate derivation reads,
and a re-derivation prints the same 79 commands. A no-driver
`merge-tree` of the head onto `ba5927f714`, from a bare shared clone,
exits 0. So there is no second merge.
- **Open PRs, re-read at 2026-09-29T02:01Z:** 9 open PRs, and none
touches any of the six files. The `data/` files open PRs touch are
objectstack-ai#20458's `analytics*` files, objectstack-ai#20504's `driver/turso.*`, and objectstack-ai#20545's
`filter-number-comparand-declared-type.*`, which is disjoint. Since the
claim, PR objectstack-ai#20460 has landed (`ba5927f714`) without touching
`filter-subtree-provenance.ts`. That file's 3 dead sites are outside
this claim's fence, so they are left for a later stage.
- **The rung.** Two anchored changes also have ADR-0087 entries in
`packages/spec/src/migrations`: `cbp-master-detail-required-forced` for
objectstack-ai#8772, and `form-view-option-default-retired` for objectstack-ai#12868. The second
entry's own header names commit `c459da6bc`. This PR takes the commit
rung, as stages 1 to 3 did. The D3 id is the more durable in-repo
record, if the ruling's first rung is later read to include those
entries.
- **What stays in `data/` after this stage: 58 dead sites.**
- **12 comment sites in files other open work still holds.**
`analytics.zod.ts`, `analytics-strictness-batchd.test.ts` and
`analytics-date-range-two-bound-window.test.ts` hold 5 (objectstack-ai#20300, PR
objectstack-ai#20458). `driver/turso.zod.ts` and `driver/turso.test.ts` hold 4
(objectstack-ai#20437, PR objectstack-ai#20504). `filter-subtree-provenance.ts` holds 3. It was held
by objectstack-ai#20367 and is now free (see above).
- **3 comment sites stage 3 left on purpose.** They are the test-read
`[objectstack-ai#6259]` marker at `api-derivation.ts:163`, the test comment at
`api-derivation.test.ts:232` that names it, and `field.zod.ts:370`,
whose `objectstack-ai#6111` is objectui's number.
- **43 string sites**, left as tokens: 41 test strings (2 of them in the
held analytics and turso test files) and the 2 exported
`AGGREGATION_CASES` note strings in `aggregation-conformance.ts`
(`:398`, `:407`, objectstack-ai#11065), which objectstack-ai#20489's claim holds.
- **Outside `data/`,** the card's other remaining items are unchanged:
the migrations and ui areas, the `liveness/**` notes, the `why` strings,
the `PROVENANCE_WAIVERS` reason, and `rest-server.zod.ts`.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`. No
test file is touched here, so all 3 touched files are in its judging
population.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…Ids derived, so two retirements merge clean (objectstack-ai#20572)

Fixes objectstack-ai#20535
Clause-②: no

Every major-18 retirement appended to two tails of `step18` in
`packages/spec/src/migrations/registry.ts`: the `+`-chained `rationale`
(by rewriting its closing line) and the `conversionIds` list. So any two
retirement PRs in flight conflicted in GitHub's driver-free merge. This
PR reshapes both tails so that two retirements no longer touch the same
line. Nothing a consumer reads changes: the values are byte-identical.

## What changed

- **`rationale` is now `STEP18_RATIONALE`.** It holds 46 fragments of
the form `{ id, order, text }`, one per retirement. The list is kept
**sorted by `id`**, and the rationale renders by `order` (ties broken by
`id`), joined with one space (`joinRationale`). Of the 46 keys, 40 are
the retirement's own D3 semantic entry id. The other 6 are kebab-case
names for retirements with no entry of their own:
`compliance-deadline-keys-retired`, `cron-positions-deleted`,
`duration-keys-unit-in-key`, `element-filter-retired`,
`element-form-retired`, `page-component-filter-record-to-rule-array`.
The fragments keep the original literal source bytes; only the 45
boundary spaces moved into the join.
- **`conversionIds` is derived:** the ids of `CONVERSIONS_BY_MAJOR[18]`,
in its order. It was a value-identical copy of that list (same 45 ids,
same order), so a retirement now adds its conversion in one place only.
This adds a second value import to `registry.ts`. It creates no cycle:
`conversions/registry.ts` imports nothing from `migrations/`, and it was
already in the migrations barrel's graph through `chain.ts`.
- The header note and the import comment now describe step 18's shape.
`MigrationStep`'s type is unchanged, and no reader of `rationale`
changed.

## Why sorted, and not the plain array the triage sketched (mechanism
measured, then the route changed)

Git reports a conflict whenever two branches insert into the **same
gap** between unchanged lines, whatever they insert. A list appended at
its end is a single gap, so a plain array conflicts exactly as the old
tail did. I measured this on a toy file and again on the real file (the
pin's end-append control below): exit 1. With the list kept sorted by
key, two retirements insert into different gaps and merge clean. One
existing fragment between them is enough, the same property
`.gitattributes` records for the sorted generated tables. Two keys that
land in the same gap still conflict. That residue is pinned as a lit
control.

## Rendered-text proof (byte-identical)

| value | parent `6154165484` | head |
|---|---|---|
| `MIGRATIONS_BY_MAJOR[18].rationale` | 48,953 chars, sha256
`797afbe924eef185…75828e10` | identical |
| `MIGRATIONS_BY_MAJOR[18].conversionIds` | 45 ids, sha256
`55d56175bf7c109c…` | identical |
| chain hop 17 → 18 `rationale` (what `migrate meta --step` prints) |
`797afbe924eef185…` | identical |
| the whole `MIGRATIONS_BY_MAJOR` value as JSON | `d989a2b827fd7f93…` |
identical |
| built `dist/index.js` + `dist/browser/index.js` (CJS) and
`dist/index.mjs` (ESM) | n/a | load; `797afbe9…` / 45 ids `55d56175…` |

No committed artifact embeds step 18's rationale: the upgrade guide
prints majors up to `PROTOCOL_MAJOR` (17). `check:upgrade-guide`,
`check:spec-changes` and `check:migration-registry` are green. A closure
check on the built bundles: all four bundles that carry step 18
(`dist/index.{js,mjs}` and `dist/browser/index.{js,mjs}`) already
carried the conversions registry. The marker was
`page-kind-jsx-to-html`, which no other non-test `src` module contains.
So the new import widens no entry's closure.

## Merge measurement: the card's instrument

A one-shot run on the **parent** `6154165484`, with git 2.43.0, in a
scratch repo holding the real file with no attributes and no driver.
Each side makes the edit a retirement PR makes:

| pair | `git merge-tree --write-tree` |
|---|---|
| two rationale-tail rewrites (closing line rewritten, sentence
appended) | **exit 1**, CONFLICT (content) |
| two `conversionIds` tail appends | **exit 1**, CONFLICT (content) |
| both edits on each side | **exit 1**, CONFLICT (content) |

The **permanent pin** is
`packages/spec/scripts/step18-rationale-merge.test.ts`, in the repo
project beside `count-shards-merge.test.ts` (PR objectstack-ai#20532), and it works
against the REAL file. It asserts:

- The premise: fragments are strictly sorted and kebab-case; the step
renders them by `order`, joined with one space (so this compares the
join, not the list); and `conversionIds` is the derived expression.
- The card's reproduction, now clean: two retirement-shaped insertions
one existing fragment apart, both taking the same next `order` → **exit
0**. The merged bytes equal both insertions applied together, and the
two render last, in key order.
- Lit controls, all **exit 1** with conflicted path `registry.ts`: a
same-gap pair; the same two fragments appended at the list's END; and
the old `+`-chain tail rewrite (a synthetic model of the parent shape).

The one open PR on this file, PR objectstack-ai#20504 (a step-18 semantic entry in a
generated region), merges clean with this head: bare shared-clone probe
with no driver, `merge-tree` exit 0.

## How a retirement adds its sentence once this lands

Add ONE element to `STEP18_RATIONALE`:

- `id` is the retirement's D3 semantic entry id.
- Insert it where that `id` sorts, **never at the end**.
- `order` is one more than the highest present. Two PRs in flight may
take the same number; they then render in `id` order.
- `text` has no leading or trailing space.

Add the D2 conversion to `CONVERSIONS_BY_MAJOR[18]` only. A branch cut
before this lands meets the change once, on its next base merge: its
appended sentence becomes one new fragment, and its `conversionIds` line
is dropped.

## Tests and gates (final commit `bcb255881a`; `registry.ts` blob
`2f010628be9a` unchanged since `2e6251af0c`)

- `@objectstack/spec` `local` project: 574 files, 16,879 passed, 1 todo
(exit 0). `repo` project: 41 files, 725 passed (exit 0). Both ran
through `os-verify-lock`, `--maxWorkers=2`, on a shared box.
- `pnpm --filter @objectstack/spec typecheck`: exit 0 (includes
`check:scripts-typecheck` and `check:test-typecheck`).
`check:generated`: 15 of 15 artifacts up to date, measured against the
`dist` built at this head.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`, reconciled with `--ran` (exit codes recorded): 88 derived,
**84 exit 0**, 4 NOT MEASURED, 0 unrun.
- NOT MEASURED (exit 3, `PREREQUISITE NOT MET`: they need the whole-repo
build closure, which CI builds):
- `check:doc-formula-expressions`: needs `@objectstack/formula` and
`@objectstack/lint` built.
- `check:dual-build-cjs-loads`: needs all packages built. Narrowed
reading: spec's own built CJS entries load (table above).
  - `check:lean-entry-closure`: needs `@objectstack/objectql` built.
- `check:type-check-debt`: needs the whole-repo build closure. Spec's
own typecheck is green.
- Declared to CI:
`packages/cli/test/migrate-meta-default-range.test.ts`. It spawns the
CLI (integration tier, and this diff touches no CLI file), it passes no
`--step`, and it reads the spec values proven identical above.
Repo-level `pnpm lint` is CI-owned.
- **Ablations** (one-shot; each through `scripts/ablation-replace.mjs`
with the anchor proven to hit, and restored to the HEAD blob with `git
diff HEAD` empty):
1. Deleting the `order` sort in `joinRationale` (render by position):
the pin went **red**, 1 failed / 8 passed, on the render-order
assertion.
2. Renaming the first key `action-aria-retired` to
`zz-action-aria-retired`: **red**, 3 failed / 6 passed. The sortedness
assertion failed, plus the two that depend on a sorted list. The first
attempt was a no-op the tool refused, because the anchor also matched
the D3 entry of the same id and nothing was written. It was re-run with
a longer anchor.

## Acceptance notes

- **Governed wording to route to the skills lane (not edited here):**
`.claude/skills/spec-property-retirement/SKILL.md:215-216` reads 「把 id
加进 `MIGRATIONS_BY_MAJOR[N].conversionIds`,扩写该步的 `rationale`。」. For N =
18 that becomes: add a `STEP18_RATIONALE` fragment at its sorted
position, and add the conversion only to `CONVERSIONS_BY_MAJOR[18]`.
Lines 217-219 (a misspelled step id is silently skipped at replay) no
longer apply to step 18, whose ids are derived.
- **Same-family residue outside this card's file surface:**
`packages/spec/src/conversions/registry.ts` has the same tail. Every
retirement with a D2 conversion appends to `CONVERSIONS_BY_MAJOR[18]`
(and usually defines its conversion just above the previous last one).
Two synthetic appends to that tail, on parent `6154165484`: `merge-tree`
**exit 1**, CONFLICT (content). So after this lands, such PRs still
conflict in that file. Only the migrations-registry half is removed
here. The order of that list is application order, so the shape there is
its own decision. Reported to the seat, not filed.
- **Choice surfaced for review:** I derived `conversionIds` instead of
giving it the keyed fragment treatment. A keyed copy would keep a second
hand-kept order, which can drift from the loader's: step 17's copy names
the same 57 ids in a different order from index 21 on. It would also let
two concurrent conversions tie-break by key instead of by the author's
chosen application order.
- The sortedness check is an assertion in the new repo-project test, not
a `check:*` gate. It is what makes an end-append fail loudly instead of
quietly bringing the conflict back.
- A side effect, not claimed as a goal: step 18's rationale was one `+`
chain of 614 literals, and its longest chain is now 28. Step 17's
970-literal chain (the `eslint.config.mjs` stack-size note) is
untouched.

---

_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… notes (objectstack-ai#20667)

Fixes objectstack-ai#20622

Clause-②: no

One new `patch` changeset (`@objectstack/cli`, in the fixed release
group) and nothing else. No code, no docs pages, no edit to any existing
changeset.

## What it carries

1. An upgrade line: the raised dependency floors cover what objectstack
loads; a lockfile-preserving upgrade can keep an older `hono` under
`@modelcontextprotocol/sdk` (via `@objectstack/cli` to
`@objectstack/mcp`), which objectstack never loads. `pnpm update hono`
clears a scanner. It states no version number.
2. A section naming, by PR number and title only, the 16 changesets (15
PRs) that shipped inside 17.5.0 without being consumed. Breaking entries
first: objectstack-ai#20458, objectstack-ai#20504, objectstack-ai#20567 (two changesets).

## Measurement

The brief's range command (`git log --diff-filter=A --name-only
8c87d26..0f6dcac -- .changeset/`) yields only 8 files. The other 8
were added BEFORE the version commit and were already left unconsumed by
it (the tree at `8c87d26a5d` still holds them). The set that shipped in
17.5.0 and is still pending is the changeset directory at `0f6dcac5e9`
intersected with `origin/main`: 16 files, all still pending, matching
the triage's 16 and its breaking set (3 PRs, 4 files). Breaking was
decided by each file's text (`BREAKING` banner / narrowing arm).

Code anchors for the upgrade line: `packages/mcp/package.json` depends
on `@modelcontextprotocol/sdk ^1.30.0`; `packages/cli/package.json`
depends on `@objectstack/mcp`;
`packages/plugins/plugin-hono-server/package.json` carries `hono
^4.13.5`; `packages/mcp/src` imports only `server/mcp`, `server/stdio`,
`server/webStandardStreamableHttp` and `types` from the SDK (no
`server/streamableHttp`).

## Gates

19 derived by `dispatch-gates.mjs --commands`, all 19 run and exit 0
(adr-0087-registration, changeset-no-major, closing-keyword-parity,
comment-mask-corpus, empty-changeset, gate self-tests, nul-bytes,
published-files and the rest); `--ran` reconciliation: 19 derived, 19
run, 0 NOT-MEASURED, 0 UNRUN. `check-changeset-fixed` green. Ordering:
must land before objectstack-ai#20639 (Version Packages, open at the time of writing).

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

2 participants