Skip to content

Commit f1f2ed0

Browse files
committed
refactor(spec): codemod split of conversions/registry.ts into per-entry modules (#29987)
WIP: the codemod and its generated split, before the reader census. Claude-Session: https://claude.ai/code/session_01WGufzg1QFj9ukYzafVoAqg Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5cdaddb commit f1f2ed0

115 files changed

Lines changed: 19211 additions & 15783 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// [#29987 codemod] The `flow-node-crud-filter-alias` conversion (`toMajor:
4+
// 11`), moved out of `conversions/registry.ts` whole by
5+
// `scripts/codemod-split-conversions-registry.mjs` (ruling 6583399215:
6+
// per-entry files).
7+
//
8+
// A MOVE, not a redesign: each declaration below is its `registry.ts` text
9+
// exactly, with the comments above it, in the order it stood there. A LEAF: it
10+
// imports neither `registry.ts` nor another conversion's module. `registry.ts`
11+
// imports `flowNodeFilterAlias` and lists it where it stood.
12+
13+
import type { MetadataConversion } from '../../types.js';
14+
import { mapFlowNodes, renameConfigKey } from '../../walk.js';
15+
16+
/**
17+
* CRUD flow-node `config.filters` → `config.filter` (protocol 11.0).
18+
*
19+
* This entry demonstrates ADR-0087's **PD #12 retirement path** (issue #2645):
20+
* the `get_record` / `update_record` / `delete_record` executors historically
21+
* tolerated the `filters` alias via a consumer-side
22+
* `readAliasedConfig(cfg, …, 'filter', ['filters'], …)` fallback. That scattered
23+
* dialect tolerance is promoted here into one declared, expiring conversion and
24+
* the executor fallback is deleted: the load path now hands the executor the
25+
* canonical `filter` key, so the executor reads `cfg.filter` directly.
26+
*
27+
* **Retired from the load path at 18** — its window (protocol 11) ran long
28+
* ago, and the entry stayed live only because nothing marked it (#23487). The
29+
* authoring funnel no longer rewrites `filters`, so an author meets the CRUD
30+
* node's strict config contract, whose refusal names `filter` and this release
31+
* (`CRUD_ALIAS_GUIDANCE`), at every door that judges a flow: `os validate` /
32+
* `os build` / `os lint`. Stored flows keep healing: the automation engine's
33+
* flow rehydration and stored-row rehydration (`applyConversionsToStoredItem`)
34+
* replay the full chain, this entry included, and so does the
35+
* artifact-ingestion door inside its window. `os migrate meta` does not reach
36+
* it: protocol 11 is below `MIGRATION_SUPPORT_FLOOR`.
37+
*/
38+
const flowNodeFilterAlias: MetadataConversion = {
39+
id: 'flow-node-crud-filter-alias',
40+
toMajor: 11,
41+
retiredFromLoadPath: true,
42+
retiredAfter: '17.7.0',
43+
surface: 'flow.node.config.filter',
44+
summary: "CRUD flow-node config key 'filters' → 'filter'",
45+
apply(stack, emit) {
46+
const crudTypes = new Set(['get_record', 'update_record', 'delete_record']);
47+
return mapFlowNodes(stack, (node, path) => {
48+
if (typeof node.type !== 'string' || !crudTypes.has(node.type)) return node;
49+
const renamed = renameConfigKey(node, 'filters', 'filter');
50+
if (!renamed) return node;
51+
emit({ from: 'filters', to: 'filter', path: `${path}.config.filter` });
52+
return renamed;
53+
});
54+
},
55+
fixture: {
56+
before: {
57+
flows: [
58+
{
59+
name: 'purge_flow',
60+
nodes: [
61+
{ id: 'n1', type: 'start' },
62+
{
63+
id: 'n2',
64+
type: 'delete_record',
65+
config: { objectName: 'lead', filters: { status: 'stale' } },
66+
},
67+
],
68+
},
69+
],
70+
},
71+
after: {
72+
flows: [
73+
{
74+
name: 'purge_flow',
75+
nodes: [
76+
{ id: 'n1', type: 'start' },
77+
{
78+
id: 'n2',
79+
type: 'delete_record',
80+
config: { objectName: 'lead', filter: { status: 'stale' } },
81+
},
82+
],
83+
},
84+
],
85+
},
86+
expectedNotices: 1,
87+
},
88+
};
89+
90+
export { flowNodeFilterAlias };
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// [#29987 codemod] The `flow-node-http-callout-rename` conversion (`toMajor:
4+
// 11`), moved out of `conversions/registry.ts` whole by
5+
// `scripts/codemod-split-conversions-registry.mjs` (ruling 6583399215:
6+
// per-entry files).
7+
//
8+
// A MOVE, not a redesign: each declaration below is its `registry.ts` text
9+
// exactly, with the comments above it, in the order it stood there. A LEAF: it
10+
// imports neither `registry.ts` nor another conversion's module. `registry.ts`
11+
// imports `flowNodeHttpRename` and lists it where it stood.
12+
13+
import type { MetadataConversion } from '../../types.js';
14+
import { mapFlowNodes } from '../../walk.js';
15+
import { FLOW_RESERVED_NODE_TYPES } from '../../../automation/flow-reserved-node-types.js';
16+
17+
/**
18+
* Flow callout node type rename (protocol 11.0; retired from the load path at
19+
* 18, #23543 F5-http).
20+
*
21+
* The divergent `http_request` / `http_call` / `webhook` node types were
22+
* unified to the single canonical `http` node (see
23+
* `services/service-automation/src/builtin/http-nodes.ts`). A pure enum
24+
* re-spelling — losslessly convertible.
25+
*
26+
* ## Retired from the load path at 18 (#23543, ruling F5-http A)
27+
*
28+
* Through 17 it stayed live because `flow.node.type` is an open namespace
29+
* (ADR-0018) and no door refused the three names, so retired alone it would
30+
* have left an authored node silently carrying a type no builtin executor owns
31+
* (the verdict #23487 recorded). The ruling reserved the names inside the open
32+
* namespace: the flow parse refuses each one as a node `type` with the
33+
* prescription naming `http`, and the automation engine refuses to register a
34+
* node executor under one (`FLOW_RESERVED_NODE_TYPES`,
35+
* `automation/flow-reserved-node-types.ts`, the table this entry reads too). So
36+
* the authoring funnel (`defineStack`, `os validate` / `os build`) no longer
37+
* replays this entry, and an author is taught the canonical type instead of
38+
* being rewritten. Stored flows are the `includeRetired` seams: the automation
39+
* engine's flow rehydration and stored-row rehydration
40+
* (`applyConversionsToStoredItem`) still rewrite a `sys_metadata` flow row
41+
* written with an old name before anything parses it, and so does the
42+
* artifact-ingestion door inside its window. `os migrate meta` does not reach
43+
* it: protocol 11 is below `MIGRATION_SUPPORT_FLOOR`. The family's D3 entry,
44+
* `flow-node-http-callout-types-reserved`, states the edit for authored sources.
45+
*
46+
* The rewrite is unconditional. Until 18 it skipped a node whose old name a
47+
* live executor owned and reported a conflict instead (ADR-0078's guard); the
48+
* engine now refuses that registration, so no live executor can own a reserved
49+
* name and the guard had no case left to judge. Protocol 18 removed the
50+
* conflict channel itself with it (#24001, D3 entry
51+
* `conversion-conflict-channel-removed`).
52+
*/
53+
const flowNodeHttpRename: MetadataConversion = {
54+
id: 'flow-node-http-callout-rename',
55+
toMajor: 11,
56+
retiredFromLoadPath: true,
57+
retiredAfter: '17.7.0',
58+
surface: 'flow.node.type',
59+
summary: "flow callout node types 'http_request' / 'http_call' / 'webhook' → 'http'",
60+
apply(stack, emit) {
61+
return mapFlowNodes(stack, (node, path) => {
62+
const type = node.type;
63+
if (typeof type !== 'string' || !Object.prototype.hasOwnProperty.call(FLOW_RESERVED_NODE_TYPES, type)) return node;
64+
const canonical = FLOW_RESERVED_NODE_TYPES[type]!;
65+
emit({ from: type, to: canonical, path: `${path}.type` });
66+
return { ...node, type: canonical };
67+
});
68+
},
69+
fixture: {
70+
before: {
71+
flows: [
72+
{
73+
name: 'notify_flow',
74+
nodes: [
75+
{ id: 'n1', type: 'start' },
76+
{ id: 'n2', type: 'http_request', config: { url: 'https://example.com' } },
77+
{ id: 'n3', type: 'webhook', config: { url: 'https://hooks.example.com' } },
78+
],
79+
},
80+
],
81+
},
82+
after: {
83+
flows: [
84+
{
85+
name: 'notify_flow',
86+
nodes: [
87+
{ id: 'n1', type: 'start' },
88+
{ id: 'n2', type: 'http', config: { url: 'https://example.com' } },
89+
{ id: 'n3', type: 'http', config: { url: 'https://hooks.example.com' } },
90+
],
91+
},
92+
],
93+
},
94+
expectedNotices: 2,
95+
},
96+
};
97+
98+
export { flowNodeHttpRename };
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// [#29987 codemod] The `page-kind-jsx-to-html` conversion (`toMajor: 11`),
4+
// moved out of `conversions/registry.ts` whole by
5+
// `scripts/codemod-split-conversions-registry.mjs` (ruling 6583399215:
6+
// per-entry files).
7+
//
8+
// A MOVE, not a redesign: each declaration below is its `registry.ts` text
9+
// exactly, with the comments above it, in the order it stood there. A LEAF: it
10+
// imports neither `registry.ts` nor another conversion's module. `registry.ts`
11+
// imports `pageKindJsxToHtml` and lists it where it stood.
12+
13+
import type { MetadataConversion } from '../../types.js';
14+
import { mapPages } from '../../walk.js';
15+
16+
/**
17+
* Page `kind: 'jsx'` → `kind: 'html'` (protocol 11.4; retired from the load
18+
* path at 18, #23543 F5-jsx).
19+
*
20+
* `'jsx'` was the first name of the constrained-JSX page tier (ADR-0080), which
21+
* ADR-0081 renamed `'html'`. The `source` semantics are identical, so the
22+
* rename is lossless.
23+
*
24+
* ## Retired from the load path at 18 (#23543, ruling F5-jsx A)
25+
*
26+
* Through 17 it stayed live because `'jsx'` was still a declared member of
27+
* `PageSchema.kind` and of `COMPILED_PAGE_KINDS`, so retired alone it would
28+
* have left the value silently accepted at every door (the verdict #23487
29+
* recorded). The ruling retired the value: `PageSchema.kind` declares it
30+
* through `enumWithRetiredValues` with the prescription naming `html`
31+
* (`PAGE_KIND_RETIRED_GUIDANCE`, `ui/page.zod.ts`), and `COMPILED_PAGE_KINDS`
32+
* and every reader that branched on it dropped the member. So the authoring
33+
* funnel (`defineStack`, `os validate` / `os build` / `os lint`) no longer
34+
* replays this entry, and an author is taught the canonical kind instead of
35+
* being rewritten. Stored pages are the `includeRetired` seams: stored-row
36+
* rehydration (`applyConversionsToStoredItem`) still rewrites a `sys_metadata`
37+
* page row written as `jsx` before anything parses it, and so does the
38+
* artifact-ingestion door inside its window. `os migrate meta` does not reach
39+
* it: protocol 11 is below `MIGRATION_SUPPORT_FLOOR`. The family's D3 entry,
40+
* `page-kind-jsx-retired`, states the edit for authored sources.
41+
*/
42+
const pageKindJsxToHtml: MetadataConversion = {
43+
id: 'page-kind-jsx-to-html',
44+
toMajor: 11,
45+
retiredFromLoadPath: true,
46+
retiredAfter: '17.7.0',
47+
surface: 'page.kind',
48+
summary: "page kind 'jsx' → 'html' (ADR-0080 canonical spelling)",
49+
apply(stack, emit) {
50+
return mapPages(stack, (page, path) => {
51+
if (page.kind !== 'jsx') return page;
52+
emit({ from: 'jsx', to: 'html', path: `${path}.kind` });
53+
return { ...page, kind: 'html' };
54+
});
55+
},
56+
fixture: {
57+
before: {
58+
pages: [{ name: 'landing', kind: 'jsx', source: '<div>hi</div>' }],
59+
},
60+
after: {
61+
pages: [{ name: 'landing', kind: 'html', source: '<div>hi</div>' }],
62+
},
63+
expectedNotices: 1,
64+
},
65+
};
66+
67+
export { pageKindJsxToHtml };
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// [#29987 codemod] The `page-component-visibility-to-visibleWhen` conversion
4+
// (`toMajor: 15`), moved out of `conversions/registry.ts` whole by
5+
// `scripts/codemod-split-conversions-registry.mjs` (ruling 6583399215:
6+
// per-entry files).
7+
//
8+
// A MOVE, not a redesign: each declaration below is its `registry.ts` text
9+
// exactly, with the comments above it, in the order it stood there. A LEAF: it
10+
// imports neither `registry.ts` nor another conversion's module. `registry.ts`
11+
// imports `pageComponentVisibilityToVisibleWhen` and lists it where it stood.
12+
13+
import type { MetadataConversion } from '../../types.js';
14+
import { mapPageComponents } from '../../walk.js';
15+
import { renameVisibilityAlias } from '../shared.js';
16+
17+
/**
18+
* Page component `visibility` → `visibleWhen` (protocol 15, ADR-0089 D2).
19+
*
20+
* The page-component spelling of the same predicate. Applies to every page
21+
* component {@link mapPageComponents} reaches — `regions[].components[]`,
22+
* `slots.<slot>`, and the containers a component nests under its `properties`
23+
* (#6776, #6775). (An AI agent's `visibility` property is a different,
24+
* unrelated surface and is not touched.)
25+
*
26+
* ## Retired from the load path at 18 (#23543, ruling F4 A)
27+
*
28+
* Same terms as {@link viewVisibleOnToVisibleWhen}, retired in the same change:
29+
* `PageComponentSchema.visibility` is a `retiredKey()` tombstone naming
30+
* `visibleWhen` (`ui/page.zod.ts`) and its parse-time fold is gone, so the
31+
* authoring funnel no longer replays this entry. Stored pages are the
32+
* `includeRetired` seams (stored-row rehydration, the artifact-ingestion door
33+
* inside its window); `os migrate meta` does not reach it, protocol 15 being
34+
* below `MIGRATION_SUPPORT_FLOOR`.
35+
*/
36+
const pageComponentVisibilityToVisibleWhen: MetadataConversion = {
37+
id: 'page-component-visibility-to-visibleWhen',
38+
toMajor: 15,
39+
retiredFromLoadPath: true,
40+
retiredAfter: '17.7.0',
41+
surface: 'page.component.visibility',
42+
summary: "page component key 'visibility' → 'visibleWhen' (ADR-0089)",
43+
apply(stack, emit) {
44+
// The region→component descent this used to open-code is the shared
45+
// `mapPageComponents` walker (#5509, adopted per #5511): identical
46+
// copy-on-write contract, identical `pages[i].regions[j].components[k]`
47+
// paths, and one fewer hand-rolled traversal to keep in step with
48+
// `PageComponentSchema`'s reach.
49+
return mapPageComponents(stack, (component, path) =>
50+
renameVisibilityAlias(component, 'visibility', path, emit));
51+
},
52+
fixture: {
53+
before: {
54+
pages: [
55+
{
56+
name: 'crm_home',
57+
regions: [
58+
{
59+
name: 'main',
60+
components: [
61+
{ type: 'record:list', visibility: "page.selectedId != ''" },
62+
{ type: 'element:divider' },
63+
// Nested one container down (#6775): the predicate means the
64+
// same thing inside a card as it does at region level, so the
65+
// walk reaches it there too.
66+
{
67+
type: 'page:card',
68+
properties: {
69+
title: 'Selected',
70+
children: [{ type: 'record:detail', visibility: "page.selectedId != ''" }],
71+
},
72+
},
73+
],
74+
},
75+
],
76+
},
77+
// …and in a named slot on a slotted record page (#6776).
78+
{
79+
name: 'crm_lead_record',
80+
kind: 'slotted',
81+
regions: [],
82+
slots: {
83+
highlights: { type: 'record:detail', visibility: "record.stage == 'won'" },
84+
},
85+
},
86+
],
87+
},
88+
after: {
89+
pages: [
90+
{
91+
name: 'crm_home',
92+
regions: [
93+
{
94+
name: 'main',
95+
components: [
96+
{ type: 'record:list', visibleWhen: "page.selectedId != ''" },
97+
{ type: 'element:divider' },
98+
{
99+
type: 'page:card',
100+
properties: {
101+
title: 'Selected',
102+
children: [{ type: 'record:detail', visibleWhen: "page.selectedId != ''" }],
103+
},
104+
},
105+
],
106+
},
107+
],
108+
},
109+
{
110+
name: 'crm_lead_record',
111+
kind: 'slotted',
112+
regions: [],
113+
slots: {
114+
highlights: { type: 'record:detail', visibleWhen: "record.stage == 'won'" },
115+
},
116+
},
117+
],
118+
},
119+
expectedNotices: 3,
120+
},
121+
};
122+
123+
export { pageComponentVisibilityToVisibleWhen };

0 commit comments

Comments
 (0)