Skip to content

Commit 6da3601

Browse files
os-zhuangclaude
andauthored
docs(adr): ADR-0120 D4 amendment — the duplicate pre-flight is per index class (#11387)
D4 decides that `os migrate plan` gains a duplicate pre-flight probe per affected index. That was written for the declared class — the `recreate_index` drift ops the reconciler can see — and it is true there. It cannot reach the three `kernel:ready` runtime index tightenings in `packages/metadata-protocol`, which are invisible to the drift differ by construction. The maintainer's 2026-08-22 ruling on #8725 routes that class to `os migrate duplicates` instead, explicitly leaving `os migrate plan`'s drift contract untouched. PR #11031 shipped that behaviour; this records the split in D4's text, where the three migration modules' doc comments already point. Documentation only: no behaviour, gate or test changes. Governed surface (docs/adr/**) — draft PR, maintainer merges by hand. Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR Co-authored-by: Claude <noreply@anthropic.com>
1 parent c0714eb commit 6da3601

1 file changed

Lines changed: 41 additions & 1 deletion

File tree

docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ the per-organization meaning **NULL-safe**:
3535
| D1 | Scope is said, not inferred | `unique: 'global' \| 'organization'` on **both** spellings; bare `true` on a *declared index* is deprecated (17.x warn → protocol 18 reject) |
3636
| D2 | Stored metadata converts losslessly | ADR-0087 D2 entry rewrites declared-index `unique: true → 'global'` — byte-identical physical shape, **zero drift** |
3737
| D3 | Per-organization unique survives NULL | organization key part materializes as `COALESCE(organization_id, '__global__')` — fixes #5030 for field-level and new `'organization'` indexes alike; ships in 17.x |
38-
| D4 | Tightening migrates through ceremony | `recreate_index` drift + duplicate pre-flight in `os migrate plan`; auto-apply only on a clean probe |
38+
| D4 | Tightening migrates through ceremony | `recreate_index` drift + duplicate pre-flight, routed **per index class** — declared/differ-visible through `os migrate plan`, runtime-managed/differ-excluded through `os migrate duplicates` (2026-08-22 amendment); auto-apply only on a clean probe |
3939
| D5 | Authoring gates carry the contract | new lint rule for unscoped declared uniques (authoring-time checkable, no tenancy guessing); R10 rewritten in the new vocabulary |
4040
| D6 | Written surfaces tell one truth | the five #3696 surfaces, the pin tests, and the false single-tenant claim in `UniqueScopeSchema` are updated in the same wave |
4141
| D7 | Staged over 17.x → 18 | additive in 17.x, rejection + conversion at protocol 18 |
@@ -353,6 +353,46 @@ with pre-existing duplicate NULL-row data — data the old index wrongly admitte
353353
No constraint-*relaxing* rebuild exists under this ADR by construction; the migration
354354
planner asserts that invariant.
355355

356+
> **Amendment (2026-08-22, [#8725](https://github.com/objectstack-ai/objectstack/issues/8725) / [#11032](https://github.com/objectstack-ai/objectstack/issues/11032)) — the duplicate pre-flight is per index CLASS: `os migrate plan` for the declared, differ-visible indexes; `os migrate duplicates` for the runtime-managed ones the differ excludes by construction.**
357+
>
358+
> The sentence above — "`os migrate plan` gains a **duplicate pre-flight probe** per affected
359+
> index" — was written for the **declared** class, the `recreate_index` drift ops the
360+
> reconciler can see, and it is true there: `os migrate plan` reports a blocked tightening of
361+
> a declared organization-unique index in full, quoting the offending group and its row count.
362+
>
363+
> It cannot reach a second class. Three `kernel:ready` migrations in
364+
> `packages/metadata-protocol` tighten an index at runtime —
365+
> `ensureMetadataOverlayIndexes` (`sys_metadata`), `ensureViewDefinitionActiveIndex`
366+
> (`sys_view_definition`), `ensureSysSettingIdentityIndex` (`sys_setting`) — and every one of
367+
> them is invisible to the drift differ **by construction**. *After* the tightening,
368+
> `isRuntimeManagedIndex` excludes the index (`isSyncReproducibleIndex` is false for a partial
369+
> index and for a `COALESCE` key part over a non-tenant column), and that exclusion is
370+
> correct — without it a boot would propose rebuilding away the guarantee it just created.
371+
> *Before* it there is nothing to see either: each migration deliberately reuses the DECLARED
372+
> index's name, so the name-matched slot reads as filled whichever physical form is there. A
373+
> command that reports **drift** can therefore never report these rows — measured with a
374+
> matched control, one database carrying the same duplicate damage under both classes, where
375+
> `plan` named the declared index in full and said nothing about the runtime-managed one.
376+
>
377+
> **Ruled (maintainer, 2026-08-22, on #8725).** The pre-flight is per class:
378+
>
379+
> - **declared, differ-visible indexes** → reported through `os migrate plan`, exactly as
380+
> decided above. Its drift contract is untouched by this amendment.
381+
> - **runtime-managed indexes the differ excludes by construction** → reported through
382+
> `os migrate duplicates`, which boots read-only and owns the "inventory, never repair"
383+
> contract.
384+
>
385+
> Everything else D4 decides is unchanged and holds for both classes: the previous index stays
386+
> in place, the report names the key that is not enforced and the rows that block it, and no
387+
> op is auto-applied on a dirty probe.
388+
>
389+
> **The split has an expiry.** It exists only because these three platform indexes are
390+
> tightened at runtime rather than declared. The route #8629's ruling parked to the
391+
> ADR-0120 / v18 train — NULL-safe uniqueness declared in the spec, so a declaration states
392+
> its own row identity and no runtime migration is needed — retires all three migrations, at
393+
> which point the class collapses back into the declared one and the pre-flight has a single
394+
> route again.
395+
356396
### D5 — Authoring gates
357397

358398
a. **New rule `unique/unscoped-declared-index`** (lint + `os validate` publish gate):

0 commit comments

Comments
 (0)