fix(driver-sql): a declared index that can never be built is logged at error and reported in drift - #20519
Conversation
…in drift A declared index whose key column never materializes (a misspelt name the Studio save door admits, or a virtual formula field) was skipped by syncDeclaredIndexes at warn and dropped from drift by expectedIndexes, so a declared UNIQUE went unenforced while nothing looked wrong. - syncDeclaredIndexes logs the skip through logDurabilityFailure at error, naming the object, the index, each missing column with its reason, and whether the index is UNIQUE. - diffUnbuildableIndexes reports it as a report-only unbuildable_index drift entry (index_mismatch, needs_confirm), which apply reports skipped. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
… cell Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…ldable_index Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
SqlDriver keeps config protected, so the class is not assignable to SqlDriverLike; bootSchemaStack reaches it by duck type. Caught by the package typecheck. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
The driver now logs a skipped declared index at error, so the object form's help text, which said warning, would have been false. The en bundle is regenerated; the zh-CN, ja-JP and es-ES values are edited by hand, the one word each. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 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 e9b8e4210c018977acac97a4ca14f9996571f549 && git checkout e9b8e4210c018977acac97a4ca14f9996571f549
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fb386074f57b234c98c40938aae7a0486ad50e8b 87f6c0bfdb5cd12a89d60236bfcdbcae85ef108c && git checkout -B drift-repro fb386074f57b234c98c40938aae7a0486ad50e8b && git merge --no-ff 87f6c0bfdb5cd12a89d60236bfcdbcae85ef108c
node scripts/docs-audit/affected-docs.mjs --json fb386074f57b234c98c40938aae7a0486ad50e8b
|
… the same release The pending note said no authoring door judges indexes[].fields (not the publish door, not os validate) and that the SQL driver skips an index with a warning. The lint rule refuses a misspelt name at os validate, os build, os lint and publish, and the skip is now an error that os migrate plan reports. Measured at 7ec990a. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
…an error that drift reports The SQL driver now logs a skipped declared index at error and os migrate plan reports it, so the message tail and the rule's docblock, which said warning and dropped, would have been false in the same release. Text only: the rule, severity, id and prescription are unchanged; the one pinned string in the rule's test follows the new wording. Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #20432 body and all 15 comments (triage 5871296642 and 5875602547; claims 5876902938, 5877196132, 5879543809, 5879937791; reports 5879503827, 5879918322, 5881175217); PR #20519 body and 13-file list; the net diff ① Derived judgments1.
2.
3. The CLI. RIGHT. No CLI source in the file list; the only 4. The pins. RIGHT.
5. The admitted text edits. RIGHT, and each corrected sentence is TRUE at the head.
6. The DELIBERATE CORRECTION of 7. Docs. No statement is made FALSE; one page is now incomplete and can be carried.
Every sentence, changeset and PR body. The changeset ② Semver level
③ Boundary flags
Check-runs on 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>
…-applied set (objectstack-ai#20563) Fixes objectstack-ai#20538 Clause-②: no `content/docs/deployment/cli.mdx` now documents the report-only `unbuildable_index` drift op (added by objectstack-ai#20519) and names the complete never-applied set. ## Before / after `needs_confirm` row (category table) - before: `os migrate apply` — except `manual_widen_varchar_to_text`, which nothing applies - after: `os migrate apply` — except `manual_column_type_change` (only `os migrate multi-value-columns --apply` runs it), and `manual_widen_varchar_to_text` and `unbuildable_index`, which nothing applies New Index-drift row (after `drop_index`) - after: `unbuildable_index` — a declared index whose key column can never exist (name is not a field of the object, or a virtual `formula` field). Report-only, category `needs_confirm`; severity `error` for a UNIQUE index, `warning` for a plain one. `os migrate apply` never performs it and reports it `skipped`. A column merely not added yet is pending `add_columns` work and is not reported. `os migrate multi-value-columns` prose - before: "it isn't the only one: `manual_widen_varchar_to_text` (...) is also never applied, but has no `os migrate` subcommand of its own. This section covers the op that does." - after: names `manual_widen_varchar_to_text` and `unbuildable_index` as also never applied, with no subcommand of their own; the section covers `manual_column_type_change`. Command table row for `os migrate multi-value-columns` (same never-applied set) - before: "one of two drift ops `apply` never reconciles" - after: "one of three drift ops" ## Code anchors measured on origin/main 6154165 - `packages/drivers/driver-sql/src/schema-drift.ts:294` `unbuildable_index` member of `DriftOp` (fields `table`, `column?`, `indexName`, `unique`, `missingColumns`); `:373` listed in `INDEX_DRIFT_OPS`; `:378` in `IndexDriftOp`. - `schema-drift.ts:1970-1975` doc: classified `needs_confirm`, `os migrate apply` reports it skipped; UNIQUE is `error`, plain is `warning`. - `schema-drift.ts:1976-2020` `diffUnbuildableIndexes`: `severity: idx.unique ? 'error' : 'warning'` (`:2001`), `category: 'needs_confirm'` (`:2002`); qualifies only when a key column is absent AND never materializes (misspelt name or virtual `formula`, `:1958-1963`); a not-yet-added column is excluded (`:1965-1968`). - `packages/drivers/driver-sql/src/sql-driver.ts:13884` `applyIndexDriftOp`: `if (op.type === 'unbuildable_index') return false;`, so the entry is reported `skipped` on every dialect (call site `:13865`, dispatch `:14022`). - `manual_column_type_change` never applied: `schema-drift.ts:166-180` (no reconciler arm, "skipped, never applied ... the intended behaviour"), emitted at `:1137-1138` as severity `error`, category `needs_confirm`; `sql-driver.ts:14131` (no reconciler arm on any dialect, by decision). - `manual_widen_varchar_to_text`: `schema-drift.ts:212`, emitted `:1341-1342` as `error` / `needs_confirm`. The code agrees with the card on every point, with one correction found in review: `manual_column_type_change` is never applied by `os migrate apply` but IS applied by `os migrate multi-value-columns --apply` (`packages/cli/src/commands/migrate/multi-value-columns.ts:105-107` selects only that op; `:315-318`, `:338-339` the `--apply` path), so the row separates it from the two ops nothing applies. Follow-up commit b982ca1. Nothing was copied from the card without a read. ## Acceptance notes - CLI source and `os migrate apply`'s skip-summary wording are untouched (other lane). - Gates (re-run on b982ca1, 36 of 41 derived, all exit 0 except the one below): docs-relevant families derived by `dispatch-gates.mjs --commands` run in the foreground; all 0 except `check:skill-examples`, which exits 3 (PREREQUISITE NOT MET: needs a `@objectstack/client-react` build; not a finding, NOT MEASURED). Neither `check:docs` nor `check:docs-transcript-drift` nor `check:nul-bytes` flags the change. - Docs-only; no changeset (`content/docs/**` is not shipped in a package `files[]`). - Commit trailers are the model-free pair. ## 维护者速读(草稿) Not applicable: no managed path (`.claude/**`) is touched. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv --------- Co-authored-by: Claude <noreply@anthropic.com>
…ng key column is logged at error on the durability sink (objectstack-ai#20650) Fixes objectstack-ai#20537 Clause-②: no The remote Turso face now reports a declared index it skips, because a key column never materializes, at `error` on its existing durability sink. It used to report it at `warn` on the diagnostic sink. This is the remote half of what PR objectstack-ai#20519 did for the local face. ## What was wrong (read on `cd901d7a5`) `RemoteTransport.buildDeclaredIndexDDL` (`packages/drivers/driver-turso/src/remote-transport.ts`) plans no DDL for a declared index whose key column is not a stored column. That covers a misspelt name (`statsu` on a table whose column is `status`) and a virtual `formula` field. Skipping it is right, because DDL naming a missing column would fail the whole sync. But the skip was reported through `this.diagnosticSink`, which `TursoDriver` wires to `logger.warn`: [RemoteTransport] skipping declared index "NAME" on "TABLE" — column(s) not materialized: statsu For a UNIQUE index, every write keeps succeeding and duplicates are accepted, and the only trace was a `warn`. The same transport already has a second sink for this class, `durabilitySink` (wired to `logger.error`). Its retrofit arm uses it for a unique AND a plain index it could not create. The local face (`SqlDriver.syncDeclaredIndexes`) logs this same skip through `logDurabilityFailure` for unique and plain indexes alike. ## What changed - **`remote-transport.ts`, `buildDeclaredIndexDDL`.** The not-materialised arm now calls `this.durabilitySink?.(…)`, not `this.diagnosticSink?.(…)`. That is triage's direction: no new sink and no second rule. It writes one line per skipped index per sync, as the local face does. The line gives the index name, the object and each missing column, and says whether the index was UNIQUE. It follows the AGENTS.md "Degradation log levels" shape: - the consequence: for UNIQUE, "the uniqueness it declares is NOT enforced: duplicate rows are accepted, and nothing looks broken from the outside"; for plain, every query the index serves is a full scan while results stay correct; - the fix: make every key column a stored field of the object, or remove the index. - The doc comments on `durabilitySink` and `buildDeclaredIndexDDL` now name this arm. - `turso-driver.ts` is **not** edited. Both sinks were already wired there. - No DDL, accept set or refusal changes. The same indexes are created and the same ones are skipped. - Changeset: `.changeset/20537-remote-skipped-index-durability.md`, `@objectstack/driver-turso` **patch**. ## The dispatch's hypotheses, measured - **H1 held**, with one correction. The skip is in `buildDeclaredIndexDDL`, the planner, not the sync itself, and all four sync paths call it: `syncSchema` (new and existing table) and `syncSchemasBatch` (new and existing table). One edit covers all four, and the pins drive all four. - **H2 held.** `turso-driver.ts` has `setDiagnosticSink` going to `this.logger.warn` and `setDurabilitySink` going to `(this.logger.error ?? this.logger.warn)`, at `:1656` and `:1667` on this base. No edit was needed. - **H3 held.** The local text is in `SqlDriver.syncDeclaredIndexes`, through `logDurabilityFailure`, for unique and plain alike. The remote line uses the same consequence-then-fix order. - **H4: the plain index goes on the durability sink too.** There are two pieces of evidence: - the local face routes the plain skip through `logDurabilityFailure`, and its doc says "A plain index is DDL that was supposed to run and did not, so it takes the same channel"; - the remote retrofit arm already reports a plain index it could not create on `durabilitySink`, with the objectstack-ai#17609 rationale: queries answer correctly by scanning, nothing looks wrong, and the cost arrives as read volume. - **H5: `check:durability-log-level` does not see this site, and no vocabulary entry belongs there.** Its header says it judges `try`/`catch` blocks whose `try` calls a declared durability-critical operation. This arm has no `catch` and runs no operation: it plans no DDL. It reports through a sink receiver, which `LOGGER_RECEIVERS` does not cover by a recorded decision. So no `DURABILITY_CRITICAL_CALLEES` entry can make it visible. The gate reads 38 seams, all loud, on this head (exit 0). ## Tests `packages/drivers/driver-turso/src/remote-transport-unbuildable-declared-index.test.ts` has 18 cases. They use the real `@libsql/client` over `file::memory:`, as the declared-index parity suite does: - **The card's matrix (16 cases).** {misspelt `statsu`, `formula` column} × {UNIQUE, plain}, each over `syncSchema` and `syncSchemasBatch`, against a new table and an existing one. Each case asserts: - exactly one durability-sink line names the index, with the table and the column quoted; - `UNIQUE` is present for the unique cells and absent for the plain ones; - no diagnostic-sink line names the column or the index; - the sync resolves; - the index is absent from `sqlite_master`. - **End to end (2 cases).** `TursoDriver` in remote mode (`libsql://…` with a supplied client), through `initObjects`. The line lands on `logger.error` and never on `logger.warn`. The prose is not pinned beyond the named subjects and the `UNIQUE` word, as in the local objectstack-ai#20432 suite. Commands, run on `90e8f132f`: - `pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2 src/remote-transport-unbuildable-declared-index.test.ts`: 1 file, **18 passed**. - `pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2` (whole package): **79 files passed, 2126 passed, 22 skipped**, verdict `command-exit 0`. - `pnpm --filter @objectstack/driver-turso typecheck`: `command-exit 0`. The package `tsconfig` includes `src/**/*`, and `tsc --noEmit --listFiles` lists the new test file once. **Reverse verification (ablation).** The fix was committed first. The mutation went through `scripts/ablation-replace.mjs` in WRAP mode, with its own trap and restore. It turned the new arm's `this.durabilitySink?.(` back into `this.diagnosticSink?.(`, and nothing else. - **Landed on disk:** anchor count went 1 to 0, and `durabilitySink?.(` / `diagnosticSink?.(` counts went 2/3 to 1/4. The blob went `1c7cb45f2828` to `672bd32cd8c6`. - **Result:** predicted all 18 red. Observed **18 failed of 18**, on a comparison (`expected [] to have a length of 1 but got +0`). - **Restored:** blob equals HEAD (`1c7cb45f2828`), `git diff HEAD` is empty, and the tree is clean. - **No `dist` step:** the suite imports the subject by a relative path (`./remote-transport.js`, `./turso-driver.js`), so vitest reads `src`. ## Gates - **Derived gates.** Derived after the final commit with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `90e8f132f`: 61 commands. The dispatch-time list had 48; the 13 added come from the changeset and test-file families. All 61 ran and exited 0. `--ran` with the exit codes recorded reads: "61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)". - **Three gates needed a full build first.** `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` first exited 3 (`PREREQUISITE NOT MET`, not measured). After a full build (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 71 of 71 tasks, with the tree clean afterwards), each exited 0. - **`pnpm check:driver-conformance`, before and after:** "50 covered cell(s), 0 in the DEBT ledger, 0 exempt" both times. - **`pnpm check:durability-log-level`:** not in the derived set; run for H5. Exit 0. - **Lint**, narrowed to the two touched `.ts` files with `eslint --no-inline-config --format json`: 2 files, 0 errors, 0 warnings. - Both files are matched by the config and neither is ignored. - `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`), so this diff cannot move the verdict on any file it does not touch. - The full `pnpm lint` run is CI's. ## Acceptance notes - **The reason for a missing column is not given per column.** The local line says, per column, why it has no column ("not a field of the object" or "a formula field"), through `describeMissingIndexColumns`. That helper is exported from `schema-drift.ts` but not from `@objectstack/driver-sql`'s package entry. Reusing it would widen that package's public surface, outside this card's claimed files. A copy here would be the second copy this file's shared-normalizer imports exist to prevent. So the remote line names the missing columns and gives both possible reasons in one clause. Carrier: none. - **With no durability sink set, the skip is not logged at all.** Before, it went to the diagnostic sink. `TursoDriver` wires both sinks together at construction, so no composition in this repo changes. The retrofit arm already works this way, and the `durabilitySink` doc says what still surfaces a missing UNIQUE (the enveloped `conflictKeys` refusal). - **Remote drift detection is not in this card.** The remote face refuses it by design, so `os migrate plan` still cannot show the skipped index on a remote datasource. Only the log line changes here. - **The branch is two commits behind `main`** (`89801cd96`, `0cb72cfc7`: service-automation and spec migration text). Neither touches `packages/drivers`, so no merge was made. CI validates the merge ref. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20432
Clause-②: yes (widening)
Step 2 of 2 on this card, and the step that completes it. Step 1, the lint refusal, landed as PR #20479.
What was wrong (measured on
origin/main9bf5e67, SQLite)Take an object whose only column is
statusand that declaresindexes: [{ fields: ['statsu'], unique: true }]:initObjectsskipped the whole index and logged one warn:[sql-driver] skipping declared index on "t_probe" — column(s) not materialized: statsu.detectManagedDrift, which is whatos migrate planrenders, returned[], becauseexpectedIndexesdrops the index from the expected set.formulafield named in an index was skipped the same way, also at warn.So the declared uniqueness was not enforced, and every instrument said nothing was wrong. AGENTS.md "Degradation log levels" asks: does the system still look normal from the outside while something it claims is persisted has not actually landed? Yes, so the level is
error. The duplicate-row arms of the same loop already answered their version aterrorthroughlogDurabilityFailure.What changed
sql-driver.ts,syncDeclaredIndexes. The skip now goes throughlogDurabilityFailure, at error, as triage's direction says. One line per skipped index per sync names:not a field of the object,a formula field: computed on read, never stored, ora declared field whose column the table does not have;UNIQUE.The line also states the consequence and the fix. A unique index gets "The uniqueness it declares is NOT enforced: duplicate rows are accepted…". A plain index gets "The index does not exist…".
The structured meta carries
{ tableName, index, fields, missing, unique }. The object'sfieldsare threaded in fromsyncTableIndexesand from the shard path; the drift-op apply paths pass none and get the bare column names. H5: a skipped non-unique index is also at error, because it is DDL that was supposed to run and did not. The message tells the two apart by theUNIQUEword and the consequence sentence, and the meta tells them apart byunique.schema-drift.ts, drift. A new pure differ,diffUnbuildableIndexes, reports the half of the declared set thatexpectedIndexesleaves out. It sharesdeclaredIndexSetwithexpectedIndexes, so the two cannot disagree about which indexes metadata asks for. An entry is emitted only when a missing key column will NEVER materialize: not a declared field, or a declared virtualformula. A declared, column-materializing field that the table merely lacks is pending additive work (add_columns), and is not reported. The entry:kind: 'index_mismatch'(an existingSchemaDiffEntryKind, so spec is unchanged),actual: '(absent)';category: 'needs_confirm'(the report-only precedent ofmanual_column_type_change);severity: 'error'when unique,'warning'otherwise (therecreate_indexconvention);DriftOpmember,{ type: 'unbuildable_index'; table; column?; indexName; unique; missingColumns }.It sits in
INDEX_DRIFT_OPS.applyIndexDriftOpanswersfalsefor it before any read, so apply reports itskippedon every dialect and it never triggers a SQLite rebuild.Public surface (H3). This is the one widening, and the seat pre-cleared it before the build: amended claim 5877196132,
Clause-②: yes (widening). The changeset is@objectstack/driver-sqlminor. No CLI source changes.The object form's help text (deviation, see below). In
packages/spec/src/data/object.form.ts, theindexes→ Fields help said the skip leaves "a warning in the server log". This change made that false, so it now says an error. The en bundle is regenerated withnode scripts/check-i18n-bundles.mjs --write. The zh-CN, ja-JP and es-ES values are edited by hand, one word each. The changeset adds@objectstack/specand@objectstack/platform-objectsas patch, and (patch round 2)@objectstack/lintas patch for the corrected rule message.Consumer census: every in-repo reader of
DriftOp,DriftOp['type']andINDEX_DRIFT_OPSI searched with
git grep(outsidedist/) forDriftOp,INDEX_DRIFT_OPS,isIndexDriftOp,ColumnDriftOp,IndexDriftOp,op.typeandManagedDriftEntry. Nonever-exhaustiveness check overop.typeexists anywhere, and noRecordis keyed byDriftOp['type'], so the new member breaks no typecheck. Measured:driver-sql,driver-tursoandclitypecheck exit 0 at 7ec990a.unbuildable_indexdriver-sqlapplyMigrationEntriesisIndexDriftOpis true, so it takes the index path, never the SQLite rebuild. Pinned.driver-sqlapplyIndexDriftOp/applyDriftOpInPlacereturn false, so the entry is reportedskipped. Pinned.driver-sqlapplyNullSafeUniquePreflightcreate_index/recreate_indexonly, so this entry is untouched.driver-sqlreconcileAndWarnDrift(boot)safeentries. This one isneeds_confirm, so it is warned once per process through the existing[schema-drift]line.driftKeyincludesindexName.schema-migrate.tsrenderPlan/driftTarget/groupByCategory/summarizecategory,op.indexName,op.type,message.os migrate planlists the entry under "Needs confirmation" astable [indexName] [unbuildable_index]. Pinned.migrate/plan.ts--jsonemits the entry as-is. The exit code does not depend on drift.migrate/apply.tsneeds_confirmentry (non-TTY without--yes: "Confirmation required"), then it is skipped, and the summary prints "Skipped N change(s)". The onlyop.typeread (line 348) is adrop_column/sys_accountfilter, which this entry does not match.multi-value-columns.tsmanual_column_type_change. Unaffected.artifact-boot-migration.ts(the artifact-pinned boot gate)category === 'destructive'. This entry is handed to the driver, reportedskipped, and warned ("schema change not applied by the driver"), and the boot continues. Pinned.driver-tursoSqlDriver, so the local face inherits the fix. The remote face already refusesdetectManagedDrift/applyMigrationEntries. Suite: 76 files, 2037 passed.The two folded boundaries
formulacolumn in an index: pinned. It is not materialized, and it is skipped. The error names it'doubled' (a formula field: computed on read, never stored), is not markedUNIQUE, and has metaunique: false. Drift reports it atseverity: 'warning'. A field-leveluniqueon a formula field takes the same route (pure-differ pin).objectExtensionsfields: not reached. Measured at BASE with the realSchemaRegistry(a throwaway probe, not committed):getAllObjects()returns the mergedfields(base and both extensions) and the mergedindexes, andSqlDriver.syncSchemaof that merged object built both indexes (uniq_h4b_account_ext_code,idx_h4b_account_ext_tier), with no skip line and empty drift.ObjectQLPluginsyncs fromregistry.getAllObjects(), so the driver only ever sees merged objects. The lint-graph gap is step 1's boundary, and sync does not reproduce it.Tests (at 7ec990a unless stated)
packages/drivers/driver-sql/src/sql-driver-20432-unbuildable-declared-index.test.ts:declareDialectCell: misspelt UNIQUE (log + drift + apply-skips), formula plain index (log + drift), and a buildable control that is enforced, with no error line;expectedIndexes, and reason kinds.Asia/Shanghaiserver zone,TZ=America/New_York): 15 passed, 1 skipped. Live MySQL: NOT MEASURED locally (no server in this container); declared to CI'sTemporal Conformance (live PG + MySQL).packages/cli/src/utils/artifact-boot-migration.unbuildable-index.test.ts(integration tier, realSqlDriverfromdist, real gate and renderer): 3 passed.driver-sqlfull suite at 2f21af6, the head before the merges (the merges bring nothing underpackages/drivers): SQLite plus live PostgreSQL gave 206 files passed / 3 skipped and 3968 tests passed / 93 skipped. SQLite only gave 198 / 11 and 3198 / 184.cliunit tier at 2062104: 233 files, 3336 passed.driver-turso: 76 files, 2037 passed / 18 skipped.spec--project local, 3 shards, at 797da30: 6070 + 5193 (+1 todo) + 5531 passed.platform-objectsat 7ec990a: 56 files, 921 passed.driver-sql,driver-turso,cli(includingcheck:test-typecheck) at 7ec990a;spec,platform-objectsat 797da30.{ type: 'unbuildable_index', … }withoutmissingColumnsgotTS2322 … Property 'missingColumns' is missing … required in type '{ type: "unbuildable_index"; …', so the CLI reads the rebuilt.d.ts. The file was removed and the tree is clean.scripts/ablation-replace.mjs(anchor 1 to 0, blob changed), trap-restored:logger.warn: 4 failed (2 per cell, SQLite and PG).columnEverMaterializesforced false: 3 failed (pure).git diff HEAD0 bytes, blobs equal to HEAD (1ede8d9d…,50ae8546…).destructive,driver-sqlrebuilt, andablation-dist-preflightconfirmed the marker in 2 built files (exit 0). CLI test 3/3 failed. After restore and rebuild,--absentexit 0 with a whole-tree clean state, and CLI test 3/3 passed.eslint.config.mjs(files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']).--format jsonread 4 files with 0 errors and 0 warnings.parserOptions.project(type-aware linting is off), so no untouched file's verdict can move. The repo-widepnpm lintis CI's.Gates (measured head 7ec990a, after merging
origin/main9449512)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 90 commands. All 90 were run, and each exit code was captured before any pipe.--ran:90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero (all 90 recorded exit 0).check-changeset-fixed, speccheck:meta-url-spelling, speccheck:spec-changes,check:authz-resolver,check:error-code-casing,check:filter-alias-parity. Pluscheck:durability-log-level, exit 0.check:driver-conformance: 50 covered, 0 DEBT, 0 exempt before and after. The dialect axis is unchanged (8 suites, 0 in the DIALECT ledger). The ledger did not rise.Patch round 1: the g2a note's index-door sentence (deliberate correction)
Admitted by the seat in the amended claim 5879543809. Text only: this round changes no code, test, help text or bundle.
The note:
.changeset/19332-g2a-fieldgroups-indexes-form-rows.md(spec lane, unreleased). One sentence on line 13 carried two clauses that are false in the same release. Nothing else in the file changes (1 line out, 1 line in).Old:
New:
The doors, measured at the patch-round base 7ec990a on the built CLI, against a one-object fixture whose UNIQUE index names
statsu(bad) orstatus(clean):os validate: bad exits 1 withobject-field-ref-unknownatobjects[0].indexes[0].fields[0], clean exits 0.os build: bad 1 (same rule and path), clean 0.os lint: bad 1 (same), clean 0.publishPackageDrafts, throwaway probe on the package's own stub engine): the bad draft is refused,INVALID_METADATAwith the same rule atobjects.idx_ticket.indexes[0].fields[0]. The clean draft ispublished.Check Changesetstays red by design on that one name.node scripts/check-empty-changeset.mjs --base origin/mainexits 1 at cef89b8. It names only.changeset/19332-g2a-fieldgroups-indexes-form-rows.md("present on the merge base and CHANGED by this PR") and prints the DELIBERATE CORRECTION class, whose remedy is "do NOT restore it -- say so on the PR and get it confirmed". This section is that statement, and the at-tier review is its written confirmation. ⛔ Noskip-changeset.Gates re-run at cef89b8 (after merging
origin/main0bbe400 with a true merge commit):dispatch-gates --commandsderived 90. All 90 were run with exit codes captured before any pipe: 89 exited 0, and 1 exited 1 (check-empty-changeset, the by-design red above).--ran: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN.check-changeset-fixed, speccheck:meta-url-spelling, speccheck:spec-changes,check:authz-resolver,check:error-code-casing,check:filter-alias-parity), and oncheck:durability-log-level,check-adr-0087-registration,check-changeset-no-majorandcheck:nul-bytes.Patch round 2: the
object-field-ref-unknownindex message (text only)The seat answered patch round 1's open question with A. Amended claim 5879937791 admits
packages/lint/src/validate-object-field-refs.ts, text only. No rule logic, severity, id, code path or test assertion moved.The message tail (the
INDEX_POSITION.consequencestring). The prescription is unchanged: the hint line reads byte-identically before and after on the fixture.The docblock sentence is put in the past tense ("the sync's skip was a
warnand drift dropped the index") and followed by a parenthesis. It says that since #20432 step 2 the skip is logged aterrorthroughlogDurabilityFailureandos migrate planreports the unbuildable index, both still after the authoring doors.The test pin: measured with
git grepacrosspackages/**,examples/**,content/**andskills/**. Exactly one test pins this text beyond the subject:validate-object-field-refs.test.tsasserted'`unique` index is then silently unenforced'. Only that pinned string changed, to'`unique` index is then unenforced'. This is a test-text change, not an assertion change: the sametoContainon the same finding. No other test, doc or skill quotes the message.Changeset:
.changeset/20432-skipped-index-durability.mdgains'@objectstack/lint': patchand one paragraph naming the corrected message.Measured at d159ac9 (
origin/mainhas not moved since cef89b8, so no merge was needed):The rule's test file passed 60 of 60. The full
@objectstack/lintsuite passed 115 files and 5363 tests.pnpm --filter @objectstack/lint typecheck(withcheck:test-typecheck) exits 0.check-changeset-fixedexits 0, andcheck-changeset-no-majorexits 0.check-empty-changesetexits 1, still on exactly.changeset/19332-g2a-fieldgroups-indexes-form-rows.md(the DELIBERATE CORRECTION class, by design).os validateon the misspelt-index fixture (built CLI) exits 1, and the clean fixture exits 0. The new tail, quoted from its output:dispatch-gates --commandsderived 91 (the new family ischeck:docs-transcript-drift). All 91 were run after a fullpackages/*andexamples/*build: 90 exited 0 and 1 exited 1 (check-empty-changeset, the by-design red).--ran: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN.The roster gates (
check-changeset-fixed, speccheck:meta-url-spelling, speccheck:spec-changes,check:authz-resolver,check:error-code-casing,check:filter-alias-parity) andcheck:durability-log-levelall exit 0.Deviations
packages/spec/src/data/object.form.ts(one help-text word and its comment) and the fourmetadata-formstranslation bundles, beyond the claim's original surface. The os-dev contract makes a published text that this change turns false a must-fix in the same PR. The breach was named in the first report instead of chosen silently, and the seat admitted it in the amended claim 5879543809 (text only, no logic). That claim also admits the deliberate correction in patch round 1 below.Acceptance notes
driver-memorymirror (measured, not touched; under its freeze).syncSchemaofindexes: [{ fields: ['statsu'], unique: true }]logs nothing at any level (onlyCreated in-memory tableat info). The index then constrains only rows that write the undeclared keystatsu. Rows duplicatingstatusare accepted silently.driver-tursoremote transport (remote-transport.ts, about line 2410) keeps its own copy of the skip line throughdiagnosticSink. It is not in this file surface..changeset/19332-g2a-fieldgroups-indexes-form-rows.mdsaid that no authoring door judgesindexes[].fields, and that the skip happens "with a warning in the server log". Both were false in the same release. Corrected in patch round 1 below, as the seat admitted.object-field-ref-unknownmessage tail on anindexes[].fieldsposition, and the rule's docblock sentence, said the skip was a warning that drift drops. The seat admitted the fix in 5879937791 (see Patch round 2).os migrate applyskip summary reads "(destructive without --allow-destructive, or unsupported on this dialect)". Neither reason is this op's. The plan message states the real one. The wording is CLI source, outside this file surface.Generated by Claude Code