Skip to content

Commit 9dacf61

Browse files
fix(spec): D3 gets one semantic entry per retirement family, even when D2 is lossless (#17152) (#20181)
Fixes #17152 Clause-②: no ## What Rewrites the ADR-0087 D2/D3 house-rule prose in `packages/spec/src/migrations/types.ts`'s module docblock — the two spots that defined a "semantic" (D3) entry as existing only for "breaks with no lossless mapping" — to state ruling B instead: **one D3 entry per retirement family, even when a lossless D2 conversion also exists for it; D2 carries the mechanical data repair only.** Per the ruling (comment `5615360777`, restated `5634031140` on #17152, authority: maintainer ruling on #15954 comment `5559778263`). ## Where the text actually was The claim's premise measurement (H1) said the header at `packages/spec/src/conversions/registry.ts:9` no longer carries the conflicting rule — confirmed: that file's module docblock (lines 3–16) is now about conversion expiry, unrelated. Searching the corpus named in the dispatch order surfaced the same house rule in **three** places, not one: 1. **`packages/spec/src/migrations/types.ts`** (hand-written, in scope) — module docblock, two spots (the intro clause and the "semantic changes" bullet under "Two feeders compose each major's step"). **Fixed here**, both spots, since they're two halves of one explanatory paragraph and fixing only one would leave the docblock self-contradictory. 2. **`packages/spec/src/migrations/registry.ts:9`** — the *exact* sentence quoted in the issue ("the semantic list is the non-lossless residue D2 could not express") lives here, verbatim, and always has (traced via `git log -S` to the original ADR-0087 commit `16b4bf69d3`). **Fixed here in round 2**, after the seat's REWORK verdict (comment `5853260202` on #17152). `scripts/build-migration-registry.ts` splices only the marked regions, and the module docblock (lines 1–14) sits outside every region. That makes it hand-written prose, and the file says so at `:46`. `gen:migration-registry` round-trips it byte for byte: the sha256 of `registry.ts` is identical before and after a regeneration. 3. **`docs/adr/0087-metadata-protocol-upgrade-contract.md`** lines 152–153 and 175–176 — the ADR itself still states the pre-ruling rule: *"Semantic changes are excluded. A change whose old shape has no lossless mapping cannot be converted; it goes to D3."* This is a governed path (`docs/adr/**`); the claim does not cover it. **Not touched**, reported per the dispatch order's governed-path instruction. Both (2) and (3) are recorded in `out_of_scope_findings` / the dev report for the PM/spec-lane triage to pick up as follow-on work; neither blocks landing this PR, since the ruling's intent (removing the conflicting *hand-written, non-generated* house-rule text this claim covers) is fully executed. ## Why Maintainer ruling on #15954 (`5559778263`), applied to this card by the director seat's class-one self-adjudication (`5615360777`, reaffirmed `5634031140`): every retirement family gets one D3 semantic entry regardless of whether a lossless D2 conversion also exists for it, so the upgrade-notes generation pipeline (which projects off the `semantic` list) never silently drops a retirement whose D2 fix happened to be lossless. ## Scope No entry added or changed, no gate added, nothing inside a generated region touched, no runtime behavior changed — comment-only. **Changeset, not `skip-changeset`** (round 2, measured by building `@objectstack/spec` and grepping `dist/**` with the `MigrationStep` / `MIGRATION_SUPPORT_FLOOR` positive controls). The `types.ts` sentence ships verbatim in `dist/index.d.ts` and `dist/index.d.mts`; the `registry.ts` sentence ships nowhere. So `skip-changeset` was removed, and `.changeset/17152-d3-per-family-house-rule.md` (`@objectstack/spec` patch) covers both edits. ## Tests - `pnpm --filter @objectstack/spec build` — PASS (see dev report `tests`). - `pnpm --filter @objectstack/spec test` — see dev report `tests`. - `pnpm --filter @objectstack/spec typecheck` — see dev report `tests`. - `node scripts/check-comment-mask-adoption.mjs`, `check-comment-mask-corpus.mjs`, `check-spec-docblock-symbol-anchors.mjs` — all PASS (comment/docblock-shape gates directly relevant to this diff). - Full derived gate list: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, reconciled with `--ran`. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ <sub>Two sentences corrected by the `domain:spec` seat 2 (`session_01QcAS3qiYYZNezaxZxaUdMV`) after round 2, as the dev's report named them: the `registry.ts:9` item and the changeset sentence.</sub> --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1c8b320 commit 9dacf61

4 files changed

Lines changed: 38 additions & 13 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Corrects the ADR-0087 D2/D3 house-rule prose across the migration chain's doc comments (`packages/spec/src/migrations/types.ts`, `packages/spec/src/migrations/registry.ts` and `packages/spec/src/migrations/spec-changes.ts`) so it states the ruled convention: every retirement family gets one D3 `semantic` entry, even when a lossless D2 conversion also exists for it, and D2 carries the mechanical data repair only (#17152).
6+
7+
Clause-②: no
8+
9+
The shape ruled superseded (director-seat class-one self-adjudication on #17152, authority: the maintainer's ruling on #15954) was stated in several places, each in its own words: `types.ts`'s module docblock said "breaks with no lossless mapping"; `types.ts`'s `SemanticMigration`/`MigrationStep`/`reason` doc comments said "A non-lossless change", "Why this is not losslessly convertible" and "Non-lossless changes authored for this major"; `registry.ts`'s module docblock said "the non-lossless residue D2 could not express"; and `spec-changes.ts`'s `SpecMigratedSchema` said "A semantic (non-lossless) migration" with a `rationale` `.describe()` of "Why it is not losslessly convertible". All are corrected to the same meaning: a D3 entry is owed per retirement family regardless of whether D2 is lossless, and `reason`/`rationale` now say why the consumer still owes a judgment rather than why the change cannot be losslessly converted.
10+
11+
No entry, gate or runtime behaviour changes; this is a doc-comment correction, filed as `patch` because the `types.ts` prose ships verbatim into the published `dist/index.d.ts` / `dist/index.d.mts` (measured: `grep` after a real build finds each corrected sentence there, with `MigrationStep` and `MIGRATION_SUPPORT_FLOOR` as positive controls proving `dist` is readable). `registry.ts`'s docblock does not ship to `dist` at this head (same positive controls, zero hits either wording). `spec-changes.ts`'s `.describe()` text ships in the runtime bundles (`dist/index.js`, `dist/index.mjs`, `dist/browser/*`), not in the `.d.ts`, `json-schema/**` or `spec-changes.json` (measured the same way, with `SpecMigratedSchema` as the `.d.ts` positive control). `check:spec-changes` / `check:upgrade-guide` / `check:authorable-surface` all report their generated artifacts unchanged. Both are corrected for the same reason — each is prose the card and the ruling target.

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* One {@link MigrationStep} per protocol major that carried a break. Each step's
77
* mechanical transforms are the D2 conversions that graduated into it (referenced
88
* by id, so the transform + fixture pair are never duplicated), and its
9-
* `semantic` list is the non-lossless residue D2 could not express.
9+
* `semantic` list carries one entry per retirement family, even when a lossless
10+
* D2 conversion also exists for it — D2 carries the mechanical data repair only.
1011
*
1112
* The chain is a **forever artifact**: every step back to
1213
* {@link MIGRATION_SUPPORT_FLOOR} stays replayable, and CI replays the full chain

‎packages/spec/src/migrations/spec-changes.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,18 @@ export const SpecConvertedSchema = z
106106
})
107107
.describe('A lossless conversion applied at load (D2).');
108108

109-
/** A semantic (non-lossless) migration (from the D3 migration chain). */
109+
/** A semantic migration, one per retirement family (from the D3 migration chain). */
110110
export const SpecMigratedSchema = z
111111
.object({
112112
surface: z.string(),
113113
replacement: z.string(),
114114
migrationId: z.string().describe('The D3 semantic-migration id.'),
115115
toMajor: z.number().int(),
116-
rationale: z.string().describe('Why it is not losslessly convertible (the load-bearing prose).'),
116+
rationale: z
117+
.string()
118+
.describe(
119+
'Why the consumer still owes a judgment here, even when D2 already repaired the data (the load-bearing prose).',
120+
),
117121
})
118122
.describe('A semantic migration requiring consumer judgment (D3).');
119123

‎packages/spec/src/migrations/types.ts‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
*
66
* Where the conversion layer (D2) breaks *invisibly* — accepting the old shape
77
* at load for one major — the migration chain is the L2 rung: *break
8-
* executably*. For the breaks D2 cannot hide (semantic changes with no lossless
9-
* mapping) and for the graduated conversions retired from the load path, the
10-
* spec ships a **permanent, ordered chain of per-major steps**, back to the
8+
* executably*. For every retirement family's semantic changes (one D3 entry
9+
* per family, even when a lossless D2 conversion also exists for it) and for
10+
* the graduated conversions retired from the load path, the spec ships a
11+
* **permanent, ordered chain of per-major steps**, back to the
1112
* chain's support floor (`MIGRATION_SUPPORT_FLOOR`, `registry.ts`). A consumer
1213
* at or above the floor runs `objectstack migrate meta --from N` and replays
1314
* every remaining step in one command, however many majors that spans — it
@@ -22,15 +23,17 @@
2223
* - **graduated conversions** — the D2 entries with `toMajor === N`, retired
2324
* from the load path in N+1 and preserved here as that major's *mechanical*
2425
* transforms (reusing the very same declarative transform + fixture pair);
25-
* - **semantic changes** — breaks with no lossless mapping, which cannot be
26-
* auto-applied and instead emit a structured TODO (surface, reason,
27-
* acceptance criteria) so the consumer agent knows exactly what judgment is
26+
* - **semantic changes** — one entry per retirement family, even when a
27+
* lossless D2 conversion also exists for it (D2 carries the mechanical
28+
* data repair only): a structured TODO (surface, reason, acceptance
29+
* criteria) so the consumer agent knows exactly what judgment is
2830
* delegated to it, rather than silence.
2931
*/
3032

3133
/**
32-
* A non-lossless change that cannot be auto-applied — surfaced as a structured
33-
* TODO the consumer agent must resolve by hand (ADR-0087 D3: "never silence").
34+
* One retirement family's D3 entry — owed even when a lossless D2 conversion
35+
* carries the data repair — surfaced as a structured TODO the consumer agent
36+
* must resolve by hand (ADR-0087 D3: "never silence").
3437
*/
3538
export interface SemanticMigration {
3639
/** Stable, kebab-case id. */
@@ -39,7 +42,10 @@ export interface SemanticMigration {
3942
surface: string;
4043
/** The canonical replacement the author should move to. */
4144
replacement: string;
42-
/** Why this is not losslessly convertible (the one load-bearing prose field). */
45+
/**
46+
* Why the consumer still owes a judgment here, even when D2 already
47+
* repaired the data (the one load-bearing prose field).
48+
*/
4349
reason: string;
4450
/** How the consumer proves the hand-migration correct (their own verify loop). */
4551
acceptanceCriteria: string;
@@ -60,7 +66,10 @@ export interface MigrationStep {
6066
* transforms are replayed against the consumer's source (not just at load).
6167
*/
6268
conversionIds: readonly string[];
63-
/** Non-lossless changes authored for this major, as structured TODOs. */
69+
/**
70+
* One entry per retirement family authored for this major, as structured
71+
* TODOs — owed even when a lossless D2 conversion also exists for it.
72+
*/
6473
semantic: readonly SemanticMigration[];
6574
}
6675

0 commit comments

Comments
 (0)