Found while auditing the alias debt named in AGENTS.md Prime Directive #12 (follow-on to #3713 / #3742 / #3754 / #3795).
Context — the shim exists and works
packages/services/service-automation/src/builtin/config-aliases.ts was built exactly for this problem. FlowNodeSchema.config is an unconstrained z.record(z.string(), z.unknown()), so the spec blesses no key — the executor is the only thing that gives config a shape at runtime. readAliasedConfig(cfg, nodeType, canonical, aliases, logger) gives that shape a declared contract: canonical wins, the alias still works for already-stored flows, and each alias warns once so authors get steered to the canonical key.
Its own doc block sets the retirement path, and it has been followed: filters → filter graduated out of the shim into the ADR-0087 D2 conversion layer (flow-node-crud-filter-alias, rewritten at load including the AutomationEngine.registerFlow rehydration seam), leaving object → objectName in crud-nodes.ts as the named next candidate.
The gap
Six alias fallbacks in the same directory never went through the shim — they are still open-coded ?? chains:
| file |
line |
fallback |
builtin/notify-node.ts |
143 |
cfg.recipients ?? cfg.to |
builtin/notify-node.ts |
144 |
cfg.title ?? cfg.subject |
builtin/notify-node.ts |
145 |
cfg.message ?? cfg.body |
builtin/notify-node.ts |
149 |
cfg.actionUrl ?? cfg.url |
builtin/screen-nodes.ts |
166 |
cfg.function ?? cfg.functionName |
builtin/screen-nodes.ts |
229 |
cfg.inputs ?? cfg.input |
Their precedence is fine — canonical is first in every one, so nothing is running the wrong branch today. That is not what makes them debt.
What they lack is everything the shim provides:
- No warning. An author who writes
to: instead of recipients: gets a flow that works forever and is never told they are on a deprecated key — the exact "fossilizes the wrong shape into a de-facto second contract and hides metadata-generation bugs" failure config-aliases.ts describes in its own header.
- Nothing declares which key is canonical. With
config unconstrained in the spec, the ?? order in the executor is the only statement of intent, and it is invisible to authoring tools, graph-lint, and skills.
- No retirement path. The shim's ledger is what makes removal schedulable; these six can never "graduate" because nothing tracks them.
graph-lint cannot gate them for the same reason — there is no declared alias list to lint against.
Suggested fix
- Route all six through
readAliasedConfig with the canonical key each ?? chain already implies, so they join the ledger and start warning.
- Add them to whatever list
graph-lint consults, so newly published flows are rejected rather than silently accepted.
- Confirm the canonical choice per pair while doing it —
inputs/input and function/functionName in particular deserve a deliberate answer rather than inheriting whatever the ?? happened to put first.
- Then treat them like
object → objectName: candidates to graduate into the D2 conversion layer once the window elapses.
Low priority — nothing is broken at runtime. It is filed so the sweep's result is recorded rather than rediscovered, per PD #10.
Note for AGENTS.md
Prime Directive #12 currently reads:
The existing cfg.filter ?? cfg.filters / cfg.objectName ?? cfg.object in the flow executors are debt to pay down, not a pattern to copy.
Both halves have moved on since that was written: filters is retired into the conversion layer, and object → objectName is no longer a raw ?? — it sits behind readAliasedConfig with a warning and a documented graduation path. The sentence is worth refreshing so it points at the shim and the conversion layer as the established retirement mechanism (and at this issue for the stragglers), rather than at two examples that no longer look the way it describes.
Refs #3713, #3742, #3754, #3795, ADR-0087.
Found while auditing the alias debt named in AGENTS.md Prime Directive #12 (follow-on to #3713 / #3742 / #3754 / #3795).
Context — the shim exists and works
packages/services/service-automation/src/builtin/config-aliases.tswas built exactly for this problem.FlowNodeSchema.configis an unconstrainedz.record(z.string(), z.unknown()), so the spec blesses no key — the executor is the only thing that givesconfiga shape at runtime.readAliasedConfig(cfg, nodeType, canonical, aliases, logger)gives that shape a declared contract: canonical wins, the alias still works for already-stored flows, and each alias warns once so authors get steered to the canonical key.Its own doc block sets the retirement path, and it has been followed:
filters→filtergraduated out of the shim into the ADR-0087 D2 conversion layer (flow-node-crud-filter-alias, rewritten at load including theAutomationEngine.registerFlowrehydration seam), leavingobject→objectNameincrud-nodes.tsas the named next candidate.The gap
Six alias fallbacks in the same directory never went through the shim — they are still open-coded
??chains:builtin/notify-node.tscfg.recipients ?? cfg.tobuiltin/notify-node.tscfg.title ?? cfg.subjectbuiltin/notify-node.tscfg.message ?? cfg.bodybuiltin/notify-node.tscfg.actionUrl ?? cfg.urlbuiltin/screen-nodes.tscfg.function ?? cfg.functionNamebuiltin/screen-nodes.tscfg.inputs ?? cfg.inputTheir precedence is fine — canonical is first in every one, so nothing is running the wrong branch today. That is not what makes them debt.
What they lack is everything the shim provides:
to:instead ofrecipients:gets a flow that works forever and is never told they are on a deprecated key — the exact "fossilizes the wrong shape into a de-facto second contract and hides metadata-generation bugs" failureconfig-aliases.tsdescribes in its own header.configunconstrained in the spec, the??order in the executor is the only statement of intent, and it is invisible to authoring tools,graph-lint, and skills.graph-lintcannot gate them for the same reason — there is no declared alias list to lint against.Suggested fix
readAliasedConfigwith the canonical key each??chain already implies, so they join the ledger and start warning.graph-lintconsults, so newly published flows are rejected rather than silently accepted.inputs/inputandfunction/functionNamein particular deserve a deliberate answer rather than inheriting whatever the??happened to put first.object→objectName: candidates to graduate into the D2 conversion layer once the window elapses.Low priority — nothing is broken at runtime. It is filed so the sweep's result is recorded rather than rediscovered, per PD #10.
Note for AGENTS.md
Prime Directive #12 currently reads:
Both halves have moved on since that was written:
filtersis retired into the conversion layer, andobject→objectNameis no longer a raw??— it sits behindreadAliasedConfigwith a warning and a documented graduation path. The sentence is worth refreshing so it points at the shim and the conversion layer as the established retirement mechanism (and at this issue for the stragglers), rather than at two examples that no longer look the way it describes.Refs #3713, #3742, #3754, #3795, ADR-0087.