Repository navigation
Commit 24d521e
refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean (#20572)
Fixes #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 #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 #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>1 parent f11b5f2 commit 24d521e
4 files changed
Lines changed: 1212 additions & 664 deletions
File tree
- .changeset
- packages/spec
- scripts
- src/migrations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
0 commit comments