Skip to content

Commit 6b97a20

Browse files
os-billclaude
andauthored
fix(spec): the 17 → 18 chain names the bare element:filter / element:form node it leaves standing (#17845)
Fixes #17594 **Clause-②: no** — an ADR-0087 D3 semantic TODO plus a disposition move. No key is added to any published payload, no accept set moves, and the refusal this TODO describes already exists at HEAD. ## What was wrong `element:filter` and `element:form` were retired whole at element grain (ADR-0049). The two ADR-0087 D2 conversions that carry the retirement — `element-filter-removed` and `element-form-removed` — strip every authorable key and **deliberately leave the bare component node**: deleting an authored page node is a layout decision a mechanical conversion must not make. That residue was inert until both names joined `RETIRED_PAGE_COMPONENT_TYPES` and `PageComponentSchema.type` began refusing them **by name**. At that point deleting the node stopped being optional and became a required step of the 17 → 18 chain — and nothing in the chain said so. ## The defect, reproduced before the change A stack carrying one node of each, driven through the real CLI (`packages/cli/bin/run-dev.js migrate meta --from 17 --to 18`): ``` --json schemaValid: false applied: 9 todos: 115 human path "Migrated stack does not yet pass schema validation — resolve the manual changes above, then run `os validate`." step-18 todos naming the node human output (396 lines) element:filter 0 0 element:form 0 0 ElementFilter 0 0 ElementForm 0 0 lit control, same corpus, same probe element:number 3 todos 6 lines element:record_picker 3 todos — ``` ⇒ the zero is a reading, not a probe that could not reach. The nine mechanical rewrites the same run attributed are all to `element-filter-removed` / `element-form-removed`, so the run really did visit both nodes and really did leave them standing. ## After Same input, same command: ``` --json schemaValid: false applied: 9 todos: 116 todos whose `surface` names BOTH node types: 1 id element-filter-and-form-node-refused toMajor 18 human path, line 150 ⚠ [protocol 18] page.component.element:filter / page.component.element:form — the bare component node itself, left standing by the `element-filter-removed` and `element-form-removed` conversions after they strip its properties → Delete the component node. `element:filter` → … `userFilters` … `element:form` → the object-bound `object-form` block … ``` It names the thing to delete rather than describing it. And the prescription is checkable: with both nodes deleted by hand and nothing put in their place, the same command reports `schemaValid: true` and the human path prints `✓ Migrated stack is schema-valid`. `schemaValid` stays `false` in the after-run **by design** — the node is still there until a human deletes it. What changed is that the author is now told which node, and what to put in its place. ## How the entry was added Per `packages/spec/src/migrations/entries/README.md`: **one new file**, named for its id, then the generator. ⛔ Nothing was typed between the `os-generated` markers. ``` packages/spec/src/migrations/entries/semantic/18.element-filter-and-form-node-refused.ts (new) pnpm --filter @objectstack/spec gen:migration-registry → ✓ wrote src/migrations/registry.ts (203 semantic, 168 retired-key, 178 retired-def) → +35 lines inside the `os-generated semantic:18` region, nothing else in the file moved pnpm --filter @objectstack/spec check:migration-registry exit 0 → build-migration-registry --self-test: ok → ✓ src/migrations/registry.ts is current (203 semantic, …) pnpm --filter @objectstack/spec check:generated exit 0 → ✓ All 15 generated artifacts are up to date. ``` `spec-changes.json` and `docs/protocol-upgrade-guide.md` are **unchanged, correctly**: both project only up to `PROTOCOL_MAJOR` (17). Lit control — the pre-existing step-18 entry `element-number-filter-rule-array` has 0 hits in either file too, so the empty diff is a property of the projection range and not of this entry. ## The ADR-0087 disposition move `.changeset/15110-retired-element-node-refusal.md` — the changeset of the PR that made the node refusable — moves from ``` not-required (already-registered element-filter-removed, element-form-removed) ``` to ``` registered element-filter-and-form-node-refused ``` The old disposition was gate-true and semantically thin: the two ids it named registered the **key strips**, not the node deletion that the refusal turned into a required step. The `registered` form the gate parses carries ids only, so the old marker's rationale sentence does not survive the move — it is superseded by the entry's own `reason` field, which states the same thing at the place a consumer reads it. `node scripts/check-adr-0087-registration.mjs --base origin/main` → exit 0 (and `--self-test` → exit 0). ## Ablation of the new pin The pin is six cases in `packages/spec/src/migrations/migrations.test.ts`. Mutation = delete the entry file and re-run the generator; the fix was committed first, so the restore point is a commit that exists. ``` HEAD blobs (restore target) entry 48c9acb registry b457da5 on-disk proof of the mutation (not an editor exit code) entry file present after rm: no occurrences of `element-filter-and-form-node-refused` in registry.ts: pre 1 → post 0 registry.ts blob after regeneration: 9b9914a — byte-identical to the pre-change registry blob, so the generator is deterministic and the whole diff to that file is this entry RED Tests 5 failed | 119 passed (124) × finds the entry (anti-vacuity …) × a 17 → 18 run emits exactly one todo naming BOTH node types (ADR-0087 D3) × prescribes DELETING the node, and names each element's replacement × ⛔ does not prescribe an automatic delete … × the acceptance criterion is checkable, and names `os validate` … restore git checkout HEAD -- BOTH_PATHS hashes read back EQUAL to the HEAD blobs above; `git diff HEAD` empty; `git status --porcelain` empty GREEN Tests 124 passed (124) ``` The sixth case — *the replay really does leave the bare nodes* — stays green through the ablation on purpose: it pins the **conversions'** behaviour, which this PR does not touch, so the block is not one undifferentiated assertion. `trap RESTORE_FN on EXIT INT TERM` with absolute paths throughout; the restore is proven by `git hash-object`, never by an exit code. No dist preflight: the pinned subject is `./registry.js` imported relatively from inside the same package, so vitest resolves it to `src/`. No `exports` hop, no `dist` on the resolution path. ## Changeset — measured, not assumed Build first (both passes confirmed: `dist/.build-input-hash` and `dist/.build-input-hash-dts` stamped at the same input hash, `check-dts-emitted: 34/34`), then `npm pack --dry-run --json` over `packages/spec` (2012 files): | probe | packed files containing it | |---|---| | **target** — the entry id | **4** (`dist/index.js`, `dist/index.mjs`, `dist/browser/index.js`, `dist/browser/index.mjs`) | | **target** — the entry's `surface` prose | **4** (same) | | **target** — `Delete the component node.` | **4** (same) | | positive control — `element-number-filter-rule-array` | 10 | | positive control — that entry's `reason` prose | 5 | | negative control — test-only prose | **0** | | negative control — the entry file's own `//` comment | **0** | | negative control — changeset-only prose | **0** | ⇒ this round's text reaches a published `dist`. Changeset written: `.changeset/17594-step18-element-node-todo.md`, `@objectstack/spec: patch`. The entry file's internal comment (the measurement provenance) lands in **no** published byte: it sits above the `import`, so the generator reads it as file scaffolding and it never enters `registry.ts`. The `packages/spec` tsup-does-not-strip-comments trap therefore does not apply here — measured, not reasoned. ## Verification | what | result | |---|---| | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | 83 commands derived | | those 83 | **83 exit 0** — two needed a re-run and are counted at their real reading, see below | | `dispatch-gates.mjs --ran` reconciliation | **83 derived, 83 run, 0 NOT-MEASURED (derived from recorded exit codes), 0 UNRUN** | | `pnpm --filter @objectstack/spec exec vitest run --project local src/migrations/migrations.test.ts` | 124 passed (118 before, +6) | | `pnpm --filter @objectstack/spec test` (the **local** vitest project) | **471 files / 13405 tests passed**, `VERDICT command-exit 0` | | `pnpm --filter @objectstack/spec test:repo` (the **repo** project — the corpus scanners) | **30 files / 520 tests passed**, `VERDICT command-exit 0` | | `pnpm --filter @objectstack/spec typecheck` | **exit 0** — includes `check:test-typecheck`, which compiles the test layer under `tsconfig.test.json`, so the new pin is type-checked rather than merely executed | | `pnpm --filter @objectstack/spec check:migration-registry` / `check:generated` | exit 0 / exit 0 | | `pnpm exec eslint . --no-inline-config` | exit 0 over **6647** files (eslint's own config decides that population), 0 errors / 0 warnings, at `bca74a4601` | Two of the 83 first came back **NOT MEASURED**, never green: - `pnpm check:dual-build-cjs-loads` → **exit 3**, `PREREQUISITE NOT MET` (twelve packages had no `dist`). Built the closure (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`) and re-ran → exit 0. - `pnpm check:type-check-debt` → **exit 143** (SIGTERM) on the first pass, then **exit 3** for the same missing-closure reason. Re-ran after the build → exit 0, 5 ledger entries re-measured, none above its recorded number. Every exit code above was captured before any pipe (`cmd > file 2>&1; EXIT=$?`), and every locked run was read from the wrapper's own `VERDICT command-exit` line, never a bare `$?`. The turbo build is a shared cache across worktrees, so `packages/spec/dist` was re-checked for this entry by occurrence count after it (1 hit in `dist/index.js`, 1 in `dist/index.mjs`) rather than assumed intact. ## Correction after the first CI run The first head (`bca74a46`) went **red** on `Test Core`, and the failure was this round's: `src/shared/retired-key-migrate-sentence.test.ts` refused the new entry's acceptance criterion. That gate scans every string literal under `packages/spec/src` (and `packages/lint/src`) for a backticked `os migrate meta --from N` marker and requires the **house tombstone sentence** at it — *Run `os migrate meta --from N` to list the mechanical edits for existing sources; apply them by hand.* The acceptance criterion named the command that way to say the upgrade is finished, so the scanner read it as a tombstone prescription. The sentence was also split across a string-concatenation seam, which `reconstruct()` merges, so both halves were judged as one. ``` before … Re-running `os migrate meta --from 17` then reports the migrated stack schema-valid instead of asking for the manual changes again after … Replaying the same 17 → 18 chain over the edited source then reports the migrated stack schema-valid — `schemaValid: true` in `--json`, and the run closes with the schema-valid line rather than the manual-changes warning ``` Derived from the gate, not from other entries by eye, and then checked against them: **no** file under `migrations/entries/` spells the `--from N` marker. Nine name the bare command or a flagged variant (`--stored`); zero name `--from N`. The house form belongs to `retiredKey()` guidance an author meets in a parse error; a D3 `acceptanceCriteria` is the consumer's verify loop, and it stays checkable — the measurement above showed that deleting both nodes makes the same command report `schemaValid: true`. ⛔ The gate was not weakened, skipped, or given a baseline entry. The sentence moved. Re-measured on head `95f4fb4b`: ``` pnpm --filter @objectstack/spec test:repo VERDICT command-exit 0 30 files / 520 tests passed (CI's red was 1 failed | 29 passed, 2 failed | 518 passed) pnpm --filter @objectstack/spec test VERDICT command-exit 0 471 files / 13405 tests passed pnpm --filter @objectstack/spec check:generated exit 0 pnpm --filter @objectstack/spec check:migration-registry exit 0 ``` Why the first round missed it: `packages/spec` declares **two** vitest projects — `test` runs `--project local`, `test:repo` runs `--project repo` — and the repo-wide corpus scanners live in the second. A green from `test` is not a reading about `test:repo`. ⚠️ One reading to discard, not to repeat: a first `test` run on the freshly re-created worktree reported `470 passed | 1 skipped`. `packages/spec/dist` did not exist yet, so a dist-dependent file skipped itself. Building spec and re-running returned the exact 471 / 13405 above — a property of the checkout, not of the edit. Unchanged by this correction: the six pin cases, the entry id, and the ADR-0087 disposition move. ## 验收备注 - ⛔ **Neither conversion's behaviour changes.** The card fences that question off itself — whether a mechanical conversion may delete an authored page node is ruling-grade — and this PR does not open it. One pin case exists specifically to hold that line. - The disposition marker in `.changeset/15110-…md` now reads `registered element-filter-and-form-node-refused` while the entry is added by **this** PR rather than that one. The gate never judges that row (its changeset is breaking at base as well as at head, so it is inherited stock and skipped), and both changesets ship in the same unreleased window, so the ledger is truthful at release time. Noted rather than filed: it is a property of moving a disposition across PRs, not a defect. - Both changesets in this diff now name the same registration id. That is deliberate — one is the PR that made the node refusable, the other is the PR that supplies its prescription — and the gate reads dispositions per file, with no cross-file uniqueness rule. --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2d34f32 commit 6b97a20

5 files changed

Lines changed: 233 additions & 1 deletion

File tree

‎.changeset/15110-retired-element-node-refusal.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,4 @@ Also corrected: the vocabulary docblock described the `ComponentPropsMap` row
7373
set as a superset of the enum by "exactly" the string-arm registrations plus the
7474
two tombstoned elements — one member short since `user:profile` joined it.
7575

76-
<!-- adr-0087: not-required (already-registered element-filter-removed, element-form-removed) both elements' retirement is already in the protocol-18 ledger — these two conversions plus all twelve retired-key tombstones; this change registers no new retirement, it closes the node-level half of those same entries and reuses their prescriptions verbatim -->
76+
<!-- adr-0087: registered element-filter-and-form-node-refused -->
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the 17 → 18 chain now NAMES the bare `element:filter` / `element:form` node it leaves behind, instead of ending schema-invalid in silence (#17594)
6+
7+
`element:filter` and `element:form` were retired whole at element grain, and the
8+
two ADR-0087 D2 conversions that carry the retirement — `element-filter-removed`
9+
and `element-form-removed` — strip every authorable key and **deliberately leave
10+
the bare component node**: deleting an authored page node changes a page's
11+
layout, which a mechanical conversion must not decide. That residue was inert
12+
until both names joined `RETIRED_PAGE_COMPONENT_TYPES` and the parse began
13+
refusing them by name — at which point deleting the node stopped being optional
14+
and became a required step of the upgrade.
15+
16+
The chain never said so. Measured on a stack carrying both nodes, before this
17+
change:
18+
19+
```
20+
os migrate meta --from 17 --to 18
21+
22+
--json schemaValid: false
23+
human path "Migrated stack does not yet pass schema validation —
24+
resolve the manual changes above"
25+
the 115 step-18 todos 0 name `element:filter`, `element:form`,
26+
`ElementFilter` or `ElementForm`
27+
```
28+
29+
ADR-0087 D3 requires a structured TODO "rather than silence" for a migration
30+
step that cannot be expressed declaratively, and this is one: only the author
31+
knows what their region should hold once the node is gone. The new
32+
`element-filter-and-form-node-refused` semantic entry supplies it — surface, the
33+
two replacements (`userFilters` for the filter, the object-bound `object-form`
34+
block for the form) and an `os validate`-clean acceptance criterion — so
35+
`os migrate meta` and the generated upgrade guide both name the thing to delete.
36+
37+
⛔ Nothing about either conversion's behaviour changes: they still strip the keys
38+
and still leave the node, and no node is deleted for the author.
39+
40+
<!-- adr-0087: registered element-filter-and-form-node-refused -->
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// #17594 — the D3 half of the node-level refusal #15110 landed. The two D2
4+
// conversions `element-filter-removed` / `element-form-removed` strip all
5+
// twelve authorable keys and DELIBERATELY leave the bare component node:
6+
// deleting an authored page node is a layout decision a mechanical conversion
7+
// must not make. That residue was inert until `element:filter` /
8+
// `element:form` joined `RETIRED_PAGE_COMPONENT_TYPES` and the parse began
9+
// refusing them BY NAME — at which point the deletion stopped being optional
10+
// and became a required step of the 17 → 18 chain.
11+
//
12+
// Without this entry the chain ends `schemaValid: false` and `os migrate meta`
13+
// closes with "resolve the manual changes above" over a manual-change list
14+
// that names neither node: measured on a source carrying both, 0 of the 115
15+
// step-18 todos matched `element:filter`, `element:form`, `ElementFilter` or
16+
// `ElementForm`, while sibling entries naming `element:number` and
17+
// `element:record_picker` matched 3 each. ADR-0087 D3 calls for a structured
18+
// TODO "rather than silence" for exactly this: a step that cannot be expressed
19+
// declaratively, because only the author knows what the region should hold
20+
// once the node is gone.
21+
//
22+
// The prescriptions are not new prose — they are the two node-level refusal
23+
// messages in `RETIRED_PAGE_COMPONENT_TYPES` (ui/page.zod.ts), so the door that
24+
// refuses and the chain that prescribes carry one instruction.
25+
26+
import type { SemanticMigration } from '../../types.js';
27+
28+
export const entry: SemanticMigration = {
29+
id: 'element-filter-and-form-node-refused',
30+
surface:
31+
'page.component.element:filter / page.component.element:form — the bare component '
32+
+ 'node itself, left standing by the `element-filter-removed` and '
33+
+ '`element-form-removed` conversions after they strip its properties',
34+
replacement:
35+
'Delete the component node. `element:filter` → a list surface owns its own '
36+
+ "filtering: use a view's `userFilters` quick-filter bar or the list toolbar's "
37+
+ 'filter builder. `element:form` → the object-bound `object-form` block, which is '
38+
+ 'rendered, designer-publishable and carries the same intent (`objectName`, '
39+
+ '`fields`, `mode`, `submitText`). Nothing is placed where the node was unless the '
40+
+ 'page needs it — which region keeps its layout is the judgment this step delegates',
41+
reason:
42+
'Both elements were retired whole at element grain (ADR-0049 enforce-or-remove): no '
43+
+ 'renderer for either ever shipped in objectui, framework or cloud, so every '
44+
+ 'authorable key was a capability claim nothing kept. The conversions are mechanical '
45+
+ 'where they can be — they strip all twelve keys losslessly — and stop at the node, '
46+
+ 'because removing an authored page node changes the LAYOUT of a page the author '
47+
+ 'composed, and a conversion cannot know whether the region should close up, hold a '
48+
+ 'replacement, or keep its slot. That residue is no longer inert: both names are '
49+
+ 'members of `RETIRED_PAGE_COMPONENT_TYPES`, so `PageComponentSchema.type` refuses '
50+
+ 'them by name, and a stack that replays the chain and stops there is schema-INVALID. '
51+
+ 'Mechanical where it can be, delegated where it cannot — this entry is the '
52+
+ 'delegation, in writing',
53+
acceptanceCriteria:
54+
'No `element:filter` and no `element:form` component remains in any page — regions, '
55+
+ 'named slots and nested containers alike (the conversions walk all three, so every '
56+
+ 'place they stripped properties is a place a bare node can be sitting). `os validate` '
57+
+ 'is clean: the refusal is reported at the node\'s `type` path with '
58+
+ '`params.retiredComponentType` naming the element, so a remaining node is named '
59+
+ 'individually rather than as one page-level failure. Replaying the same 17 → 18 chain '
60+
+ 'over the edited source then reports the migrated stack schema-valid — '
61+
+ '`schemaValid: true` in `--json`, and the run closes with the schema-valid line '
62+
+ 'rather than the manual-changes warning',
63+
};

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,99 @@ describe('migration chain (ADR-0087 D3)', () => {
211211
});
212212
});
213213

214+
// The D3 half of a node-level refusal, and the one class of entry whose
215+
// ABSENCE is invisible to every other gate in this family: `check:spec-changes`
216+
// and `check:upgrade-guide` pin the registry to its PROJECTIONS, so an entry
217+
// that was never written leaves them perfectly consistent. What made the gap
218+
// reachable is that the two D2 conversions below are deliberately partial —
219+
// they strip the props and leave the node, because deleting an authored page
220+
// node is a layout decision a mechanical conversion must not make — while
221+
// `RETIRED_PAGE_COMPONENT_TYPES` now refuses that same node BY NAME. Between
222+
// the two, a 17 → 18 replay ended `schemaValid: false` and `os migrate meta`
223+
// closed with "resolve the manual changes above" over a list that named
224+
// neither element. This block pins the instruction back into the list.
225+
describe('protocol-18 #17594 entry — the chain NAMES the bare node it leaves standing', () => {
226+
/** A page authored against 17, carrying both retired elements. */
227+
const authored = () => ({
228+
pages: [
229+
{
230+
name: 'order_board',
231+
regions: [
232+
{
233+
name: 'main',
234+
components: [
235+
{ type: 'element:filter', properties: { object: 'order', fields: ['status'] } },
236+
{ type: 'element:form', properties: { object: 'order', fields: ['status'] } },
237+
],
238+
},
239+
],
240+
},
241+
],
242+
});
243+
244+
const entry = () =>
245+
MIGRATIONS_BY_MAJOR[18]!.semantic.find((s) => s.id === 'element-filter-and-form-node-refused');
246+
247+
it('finds the entry (anti-vacuity: every assertion below reads through this `find`)', () => {
248+
expect(entry()).toBeDefined();
249+
expect(entry()!.surface).toMatch(/element:filter/);
250+
expect(entry()!.surface).toMatch(/element:form/);
251+
});
252+
253+
it('the replay really does leave the bare nodes — the residue this TODO is about', () => {
254+
const result = applyMetaMigrations(authored(), 17, 18);
255+
const ids = new Set(result.applied.map((a) => a.conversionId));
256+
expect(ids.has('element-filter-removed')).toBe(true);
257+
expect(ids.has('element-form-removed')).toBe(true);
258+
259+
// Both nodes survive the chain, stripped bare. If a conversion ever starts
260+
// deleting them this line fails, and this entry's premise is what should be
261+
// revisited — not this expectation.
262+
const components = (result.stack.pages as any[])[0].regions[0].components;
263+
expect(components.map((c: any) => c.type)).toEqual(['element:filter', 'element:form']);
264+
expect(components[0].properties).toEqual({});
265+
expect(components[1].properties).toEqual({});
266+
});
267+
268+
it('a 17 → 18 run emits exactly one todo naming BOTH node types (ADR-0087 D3)', () => {
269+
const result = applyMetaMigrations(authored(), 17, 18);
270+
const naming = result.todos.filter(
271+
(t) => /element:filter/.test(t.surface) && /element:form/.test(t.surface),
272+
);
273+
expect(naming).toHaveLength(1);
274+
expect(naming[0]!.id).toBe('element-filter-and-form-node-refused');
275+
expect(naming[0]!.toMajor).toBe(18);
276+
});
277+
278+
it('prescribes DELETING the node, and names each element\'s replacement', () => {
279+
// The two replacements are the ones `RETIRED_PAGE_COMPONENT_TYPES` already
280+
// sends an author to at the parse; pinned here so the two doors cannot
281+
// drift into prescribing different things.
282+
const r = entry()!.replacement;
283+
expect(r).toMatch(/Delete the component node/);
284+
expect(r).toMatch(/userFilters/);
285+
expect(r).toMatch(/object-form/);
286+
});
287+
288+
it('⛔ does not prescribe an automatic delete — the conversions must not make it', () => {
289+
const text = `${entry()!.replacement} ${entry()!.reason}`;
290+
expect(text).toMatch(/layout/i);
291+
expect(text).not.toMatch(/the conversion (deletes|removes) the node/i);
292+
});
293+
294+
it('the acceptance criterion is checkable, and names `os validate` (the card\'s bar)', () => {
295+
const a = entry()!.acceptanceCriteria;
296+
expect(a).toMatch(/os validate/);
297+
// Named at the node's own path, so a remaining node is reported
298+
// individually rather than as one page-level failure.
299+
expect(a).toMatch(/retiredComponentType/);
300+
// Regions, slots and nested containers — the three places the conversions
301+
// walk, and therefore the three places a bare node can be left.
302+
expect(a).toMatch(/slots/);
303+
expect(a).toMatch(/nested containers/);
304+
});
305+
});
306+
214307
describe('composition (cross-major is the designed-for case)', () => {
215308
it('composes only the steps in (from, to]', () => {
216309
const chain = composeMigrationChain(10, 11);

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7208,6 +7208,42 @@ const step18: MigrationStep = {
72087208
+ 'two records) become off-spec fixtures and `ElementDataSourceConfig.filter`\'s '
72097209
+ '"three shapes" note narrows — objectui cards filed by the seat, not blocked on here.',
72107210
},
7211+
{
7212+
id: 'element-filter-and-form-node-refused',
7213+
surface:
7214+
'page.component.element:filter / page.component.element:form — the bare component '
7215+
+ 'node itself, left standing by the `element-filter-removed` and '
7216+
+ '`element-form-removed` conversions after they strip its properties',
7217+
replacement:
7218+
'Delete the component node. `element:filter` → a list surface owns its own '
7219+
+ "filtering: use a view's `userFilters` quick-filter bar or the list toolbar's "
7220+
+ 'filter builder. `element:form` → the object-bound `object-form` block, which is '
7221+
+ 'rendered, designer-publishable and carries the same intent (`objectName`, '
7222+
+ '`fields`, `mode`, `submitText`). Nothing is placed where the node was unless the '
7223+
+ 'page needs it — which region keeps its layout is the judgment this step delegates',
7224+
reason:
7225+
'Both elements were retired whole at element grain (ADR-0049 enforce-or-remove): no '
7226+
+ 'renderer for either ever shipped in objectui, framework or cloud, so every '
7227+
+ 'authorable key was a capability claim nothing kept. The conversions are mechanical '
7228+
+ 'where they can be — they strip all twelve keys losslessly — and stop at the node, '
7229+
+ 'because removing an authored page node changes the LAYOUT of a page the author '
7230+
+ 'composed, and a conversion cannot know whether the region should close up, hold a '
7231+
+ 'replacement, or keep its slot. That residue is no longer inert: both names are '
7232+
+ 'members of `RETIRED_PAGE_COMPONENT_TYPES`, so `PageComponentSchema.type` refuses '
7233+
+ 'them by name, and a stack that replays the chain and stops there is schema-INVALID. '
7234+
+ 'Mechanical where it can be, delegated where it cannot — this entry is the '
7235+
+ 'delegation, in writing',
7236+
acceptanceCriteria:
7237+
'No `element:filter` and no `element:form` component remains in any page — regions, '
7238+
+ 'named slots and nested containers alike (the conversions walk all three, so every '
7239+
+ 'place they stripped properties is a place a bare node can be sitting). `os validate` '
7240+
+ 'is clean: the refusal is reported at the node\'s `type` path with '
7241+
+ '`params.retiredComponentType` naming the element, so a remaining node is named '
7242+
+ 'individually rather than as one page-level failure. Replaying the same 17 → 18 chain '
7243+
+ 'over the edited source then reports the migrated stack schema-valid — '
7244+
+ '`schemaValid: true` in `--json`, and the run closes with the schema-valid line '
7245+
+ 'rather than the manual-changes warning',
7246+
},
72117247
{
72127248
id: 'element-number-filter-rule-array',
72137249
surface:

0 commit comments

Comments
 (0)