fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction (#20437) - #20504
Conversation
…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
…plica-needs-syncurl
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: Read-only, at tier, on PR #20504 for card #20437 (rider of #20200), branch ① Derived judgments1. The refused shape, at both doors — right. The spec arm (
2. The driver mirror — right, with one caveat. The mirror ( 3. The flipped pins — right. (a) Parity row "file: under a forced mode: 'replica'" was 4. ADR-0087 D3 entry — new entry required: right; text: one clause wrong. The family entry 5. Producer census (in-repo), re-read at the head: no non-test file authors Changeset sentences: front matter 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 ② Semver level
③ Boundary flags
Deviations, each answered:
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 Text follow-up recommended before release, not a contract defect: the D3 entry's Check-runs on the head, final read at the end of this review (2026-09-28, after 20:26Z): 34 check-runs — 29 Implemented-by: 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
…plica-needs-syncurl
Contract reviewServed-tier: Delta review, read-only, at tier, on PR #20504 for card #20437, branch ① Derived judgments1. The PR's own delta is text only — right, and every judgment of the prior record carries over. 2. Judgment 4's wrong clause — corrected, and the corrected clause is TRUE. The 3. 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 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); 6. Report and PR-body sentences, this round. Report 5878055796: text only, the three files, the corrected ② Semver levelUnchanged and still right: ③ Boundary flags
Round-0 flags carry as answered in 5877910448, with one change of state:
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 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 Implemented-by: VERDICT: PASS |
…plica-needs-syncurl
…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
Contract reviewServed-tier: Delta review, read-only, at tier, on PR #20504 for card #20437, branch ① Derived judgments1. The merge 2. 3. The PR's net diff against the new base equals its net diff at 5dfa45e against 4a1df19, the reword aside. Right. With 4. The reword of 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 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. ② Semver levelUnchanged and still right after the merge: ③ Boundary flags
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 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 Out-of-scope notes carried, not this PR's by order: (a) the sibling entry Check-runs on the head, read once at the end of this review (2026-09-29T04:46:56Z): 42 check-runs on 519cfbc — 37 Implemented-by: VERDICT: PASS |
…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>
… 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>
…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>
… 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>
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:engineseat 1, mode:subagent), branchclaude/issue-20437-replica-needs-syncurl. The branch starts atfc0db22bcand was merged withorigin/mainat9bf5e67afin a true merge commit. Every final reading below was taken at head7bb7b3aeeunless it says otherwise.What changes
A turso config that forces
mode: 'replica'on afile:url with nosyncUrl(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"):tursoTransportIssuesinpackages/spec/src/data/driver/turso.zod.tsgains a replica arm after the in-memory one. It returns onecustomissue onmode, the key that cannot be honoured, as thesyncrefusal sits onsync. It reachesDatasourceSchema(asconfig.mode),validateDriverConfig,defineStack/os validate, and a save or test connection through the datasource admin service.new TursoDriver()refuses the same config withVALIDATION_ERROR/ 400, beforesuper(). The check sits after thesync-without-syncUrlrefusal, so a config with both defects meets thesyncmessage first, which is also the spec's first issue. The message is a module constant,REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL, next to driver-turso:new TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_NOT_SUPPORTED#20200's two, and thrown through the samerefuseIgnoredSyncKeyhelper. The parity table pins it byte-equal to the schema's issue. That is driver-turso:new TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_NOT_SUPPORTED#20200's pattern (H3).packages/drivers/driver-turso/src/spec/turso.zod.ts) carries the same arm byte for byte. The mirror stripsmode, so the arm is unreachable through it (see H4), and it stays for copy parity like the mirror's other forced-mode branches.The message, the same text at both doors:
It gives both fixes the ruling prescribes: add
syncUrl, or dropmode: '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 WIDENEDFILE:+mode 'replica', nosyncUrlcell (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 withtransportMode = '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, atdbddf02c1and again at2242ad513). The second is the source at the base:connect()builds the sync client only insideif (this.tursoConfig.syncUrl)(turso-driver.ts:1821),sync()returns early on!(this.libsqlClient && this.tursoConfig.syncUrl)(:3522), andisSyncEnabled()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 atfc0db22bc, plus every spelling that builds aTursoConfig:packages/create-objectstack(templates),skills/, hand-writtencontent/docs: zero authors of a forced replica. The onlycontent/docshit is the auto-generated reference row for themodeenum.TURSO_*/OS_DATABASE_*names read inpackages/**/srcareOS_DATABASE_URL,OS_DATABASE_AUTH_TOKEN,OS_DATABASE_DRIVER,OS_DATABASE_POOL_MAX,OS_DATABASE_SQLITE_JOURNAL_MODE,TURSO_DATABASE_URL,TURSO_AUTH_TOKENandTURSO_TOKEN. None of them maps tomodeorsyncUrl.modereaches the driver only throughbuildTursoDriverConfig'smodereader (packages/services/service-datasource/src/turso-driver-config.ts:205), which reads an authoreddatasource.config.mode. The CLI and the standalone host build their default datasource from the env url and token alone.mode: 'replica':packages/cli/src/utils/storage-driver.test.ts:478,packages/runtime/src/turso-driver-factory.convergence.test.ts:70/:102andpackages/services/service-datasource/src/__tests__/turso-driver-config.test.ts:49/:60. Every one carries asyncUrl, 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", andgit ls-remoteis 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_decisionbranch does not trigger on anything measured here.H4: every mode / url cell, before and after
file:, nomode, nosyncUrlfile:+ forcedreplica, nosyncUrlmodeand judges it local)modeFILE:urlsyncUrl: ''(unset)timeoutMssync, nosyncUrlsyncmessage (#20200)syncsyncmessage). The spec now raises 2 issues,syncthenmodelibsql://+ forcedreplica, nosyncUrlurlurlrefusal, which already names both ways out (dropmodeor set it remote; or afile:url besidesyncUrl). Not the same message, and no second refusal:memory:/file::memory:+ forcedreplicaurlreplicaurlfile:+ forcedreplica+syncUrlThe "after" column is pinned by the new test file (the refusal, ORDER and CONTROLS blocks) and by the parity table.
H5: ADR-0087 disposition —
registeredThe family's entry
turso-config-transport-mismatch-refuseddoes not cover this refusal. Itssurfaceenumerates the refused combinations (a remote url besidesyncUrlor under a forced local/replica mode, an unrecognised url, an in-memory replica,timeoutMsbeside a WebSocket url, andsyncUrlunder a forced remote mode). A forced replica on afile:url with nosyncUrlis not among them, and itsacceptanceCriterianame onlyconfig.url,config.syncUrlandconfig.timeoutMs. Itsreplacementwould 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, idturso-config-forced-replica-without-sync-url-refused, with itssurface,replacement,reason(the measurement, the order of the sibling refusals, and what a stored row now does at boot) andacceptanceCriteria(reported atconfig.mode).src/migrations/registry.tswas regenerated bygen:migration-registry(311 semantic), never by hand. Patch round 1 corrected the entry'ssurface. It had named the driver'sTursoConfigSchemamirror among the surfaces where this shape "is now refused, on mode". The mirror cannot refuse it, because it declares nomodekey and strips an authored one. Thesurfacenow 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.tswas regenerated again bygen:migration-registry. The existing entry is untouched, because it is #20200's text and stays true. The changeset carriesregistered turso-config-forced-replica-without-sync-url-refused, andcheck-adr-0087-registrationreads it as[BREAKING+bang+clause-②-narrowing] registered … (new here: …).spec-changes.jsonanddocs/protocol-upgrade-guide.mddo not project major-18 entries yet (the sibling entry is absent from both too), andcheck:spec-changes/check:upgrade-guideare green.Stored rows (read, not edited):
buildTursoDriverConfigforwards a stored row'smodeunparsed, so a row in this shape now fails atfactory.create.DatasourceConnectionServicerecords itfailed-degraded(datasource-connection-service.ts:355/:457), and a test connection answersok: falsewith "Failed to build driver: MESSAGE" (datasource-admin-plugin.ts:707). Under ADR-0062 D5 the boot fails fast when objects bind to it, unlessOS_ALLOW_DRIVER_CONNECT_FAILUREis set. The changeset's FROM → TO paragraph says so.Tests
7bb7b3aee@objectstack/driver-tursovitest, whole package@objectstack/driver-tursotypecheck (tsc --noEmit)tsc --listFilesOnly(pre-merge) counts all 4 touched test files in the program@objectstack/specvitest--project local, 3 shards@objectstack/spectypecheck (tsc + scripts +check:test-typecheck)@objectstack/speccheck:generated(pre-merge, after the spec build)The 22 skips are the parity table's forced-mode rows for the mirror, which strips
mode: 18 before, plus the 4 newmoderows.spec/turso-config-constructor-parity.test.ts: the row "file: under a forced mode: 'replica'" flips fromaccepttorefuse,refusedOn: 'mode'. Threemoderows are added (an uppercaseFILE:url, an emptysyncUrl, andtimeoutMs), plus an accept control, "file: + syncUrl under a forced mode: 'replica'". The row "sync with no syncUrl under a forced mode: 'replica'" now declaresissues: 2, a new per-row field (default 1).SYNC_KEY_REFUSALStakes themoderows, so each of the 4 asserts the constructor's message equals the spec issue's. New floors:modeat least 4, sync-key refusals at least 12.turso-driver-unrecognised-url-refusal.test.ts: the WIDENEDFILE:+mode 'replica', nosyncUrlcell leaves PRESERVATION, and the restart test keeps only itssyncUrlhalf. 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, onfile:andFILE:urls, beside an emptysyncUrl,timeout,encryptionKey, and a supplied client (the client stays untouched). It also coverscreateTursoDriver()and a url-echo check. ORDER: a remote url,:memory:,file::memory:, a bare path andsynceach keep their own refusal. CONTROLS: a forced replica besidesyncUrlconnects, reports sync on and keeps its rows across a restart; a replica selected bysyncUrl;file:with nomode;mode: 'local';:memory:; anddetectModestill answersreplica.packages/spec/src/data/driver/turso.test.ts: the accept fixture{ url: 'file:./data/replica.db', mode: 'replica' }gains asyncUrl. A new block asserts the refusal onmode(the envelope, the first sentence, both ways out), that the url refusals keep their order, the two issues besidesync, both authoring doors (config.modeandvalidateDriverConfig), and the controls.Reverse verification, via
scripts/ablation-replace.mjsfrom the committed state, pre-merge head79fbad660. Each direction was predicted before the run, and all three matched:if (mode === 'replica' && !config.syncUrl) {becameif (false && …) {, and the mutation landed (anchor 1 → 0, blob8c7f7be9→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 HEADempty.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.packages/spec/src/data/driver/turso.zod.ts(blobfdfb4a6f→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 at7bb7b3aee, lists 11 paths vs merge base9bf5e67afand 91 commands. Every command ran, with its exit code written to disk before any pipe.--ranreconciliation reads91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), with 0 UNRUN.check:dual-build-cjs-loads(dozens of workspace packages have nodist/) andcheck:type-check-debt(it needs a whole-workspace build). Both are CI's run.check:doc-formula-expressionsandcheck:lean-entry-closurefirst answered exit 3. They are exit 0 after building the@objectstack/lintand@objectstack/objectqlclosures.check-adr-0087-registration→registered turso-config-forced-replica-without-sync-url-refused(new here), BREAKING, bang, clause-② narrowing;check-changeset-no-majorexit 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-guideandcheck-empty-changeset→ exit 0.check-changeset-fixed, speccheck:meta-url-spelling,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity, all exit 0.eslint --no-inline-config --format jsonover the 10 changed TS files reports 10 files, 0 errors and 0 warnings. The population iseslint.config.mjs's lint object,files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']; the changeset.mdis outside it. Invariance holds because the config enables no type-aware linting (noparserOptions.project), so this diff cannot move any untouched file's verdict. The fullpnpm lintis CI's.@objectstack/service-datasource,@objectstack/runtimeand@objectstack/cli. The narrowing is declared: the public surface's bytes are unchanged (check:api-surfaceandcheck:authorable-surfaceare 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-conformancereadOK — 50 covered cell(s), 0 in the DEBT ledger, 0 exemptboth before (fc0db22bc) and after (7bb7b3aee).driver-tursoisokon all 10 case-sets both times. No movement.Deviations (declared)
needs_decision.issuescount (default 1) was added for the one row where the spec now raises two issues. The alternative was to suppress themodeissue whensyncalso fires, which would couple the two arms. Raising both matches how the schema reports every other independent defect.mode: the triage ruling names the message, not the key.modewas chosen by the sibling convention (thesyncrefusal sits on the present, unhonourable key).TursoTransportIssue.pathwidens to include'mode'in both copies (module-local types, no export).Acceptance notes
packages/drivers/driver-turso/README.md's list of constructor refusals named only the url refusals. Patch round 1 adds this refusal and driver-turso:new TursoDriveracceptssyncUrl/syncundermode: 'remote'and ignores them —isSyncEnabled()answers true, no sync runs, andsync()rejectsSYNC_NOT_SUPPORTED#20200's two sync-key refusals, one line each. It is docs text only and outside the claim's letter, so it is declared as a deviation.modekey keeps its one-line TSDoc. The requirement is carried by the refusal text and byTursoDriverConfig.mode's TSDoc in the driver, which now names it. That avoids a describe/TSDoc regeneration lap for no authoring gain.turso-config-transport-mismatch-refusedis untouched. Its "Nothing the constructor accepts is refused" stays true, because both doors refuse this shape together.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:turso-config-forced-replica-without-sync-url-refused: itssurfaceno 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.tswas regenerated bygen:migration-registry, and its hunk equals the entry's, re-indented. The sibling family entryturso-config-transport-mismatch-refusedis not edited here; the seat carries its same overstatement as a note.packages/drivers/driver-turso/README.md: the list of constructor refusals gains one line each forsyncUrlunder a forcedmode: 'remote',syncwith nosyncUrl, and a forcedmode: 'replica'with nosyncUrl, 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 oforigin/mainat4a1df1965. The merge touched no migrations or turso path, andgen:migration-registryafter it wrote no change. Readings at5dfa45e9f, each exit code recorded before any pipe:pnpm --filter @objectstack/spec check:migration-registryexit 0:registry.ts is current (311 semantic, 230 retired-key, 206 retired-def);pnpm check:adr-0087-registrationexit 0:[BREAKING+bang+clause-②-narrowing] registered turso-config-forced-replica-without-sync-url-refused (new here …);check:spec-changes,check:upgrade-guide,check:doc-authoring,check:nul-bytesandcheck-changeset-no-majorall exit 0;--project localoversrc/migrations,src/conversionsandsrc/data/driver: 22 files, 1044 passed, exit 0.dispatch-gates --commandsat this head derives the same 91 commands as round 0 (12 paths vs merge base4a1df1965; the README adds no family). The full union was not re-run for this text-only delta. The round-0 union at7bb7b3aeestands, and CI measures this head.Patch round 2 (merge + form D), head
519cfbce5This 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/cloudwas read or edited. The round changes no code, test, schema text, refusal message or changeset. The changeset's level and theClause-②line do not move.The merge.
origin/mainat1c761c0d7was merged into the branch in a true merge commit,04be827ce(parents5dfa45e9fand1c761c0d7), bybash scripts/pm/os-regen-merge.sh. There was no rebase and no force-push. Since the old base4a1df1965, 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$emptyvalue-shape resolver wiring and the$emptypresence-flag case. The merged file carries both sides:setDeclaredValueShapeResolverandcase '$empty':sit besideREPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL.packages/spec/src/data/driver/turso.zod.tsdid not move on main in that window. The merge left no os-regen deferral.Regeneration, with the repo's own tooling.
pnpm --filter @objectstack/spec check:migration-registryread the textually mergedregistry.tsas current (315 semantic, 232 retired-key, 206 retired-def), andpnpm --filter @objectstack/spec gen:migration-registrythen wrote no change. After the entry edit in item 3,gen:migration-registryran again, and itsregistry.tshunk is the entry's hunk re-indented (+4 / −1 in each file).spec-changes.jsonanddocs/protocol-upgrade-guide.mdstill project no major-18 entry.check:spec-changes,check:upgrade-guideandpnpm --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.Form D in the entry's
reason. The file is18.turso-config-forced-replica-without-sync-url-refused.ts, andreasonis thewhy:lineos migrate metaprints. 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:#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, …"The rest of
reasonis 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 at5dfa45e9f, which reads 1 inreason. No test asserted the old sentence, so nothing was re-pinned. The CLI pinmigrate-meta-engine-guidance.test.tsselects entries by id prefix, andturso-is not one of its covered families, before or after stage 7 on main. No other test quotes the text. The siblingturso-config-transport-mismatch-refusedstill 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.turbo run buildover the@objectstack/driver-turso,@objectstack/lintand@objectstack/objectqlclosures: 15 tasks, exit 0.@objectstack/driver-turso. Vitest over the whole package: 78 files, 2108 passed, 22 skipped, exit 0. Typecheck exit 0, andtsc --listFilesOnlycounts all 4 touched test files.@objectstack/spec. Vitest--project localin 3 shards: 574 files, 16884 passed, 1 todo (6106 + 5220 + 5558), exit 0 on each shard. Typecheck (tsc, scripts andcheck:test-typecheck) exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstacklists 12 paths vs merge base1c761c0d7and 91 commands, and all 91 ran.--ranreads "91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". The 2 NOT MEASURED arecheck:dual-build-cjs-loadsandcheck:type-check-debt, both PREREQUISITE NOT MET because they need a whole-workspace build. CI runs both.pnpm check:doc-authoringexit 0.pnpm check:adr-0087-registrationexit 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-registryexit 0 (315 semantic).check:spec-changes,check:upgrade-guide,check:release-spec-changes,check-changeset-no-major --base origin/mainandcheck:nul-bytesexit 0.check:doc-authoringdoes not read a migration entry's prose fields: patch round 1 recorded it green at5dfa45e9fwith the number still present. So the proof for item 3 is the field census above.check-changeset-fixed, speccheck:meta-url-spelling,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity, all exit 0.eslint --no-inline-config --format jsonover the 10 changed TS files reports 10 files, 0 errors and 0 warnings. The population iseslint.config.mjs'sfiles: ['**/*.{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.f11b5f20a, stage 7 of the guidance rewrite, which touchedregistry.tsand 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 --checkover the probed tree readsregistry.tsas 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