|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec)!: `FlowSchema` refuses a flow whose `edges[]` declares the same id twice (#14964) |
| 6 | + |
| 7 | +<!-- adr-0087: not-required (no-migration-prescription) No authorable key is renamed, retired or re-typed: `edges[].id` keeps its name, its type and its describe, and every flow whose edge ids are unique parses byte-identically. The only newly refused shape is two edges sharing one id — a collision, not a spelling — and its remedy is to renumber one of the two, which is authoring intent no `objectstack migrate meta` rewrite can choose for the author. The Zone-2 census over this repo (776 `edges[]` arrays, 1,098 edges under `packages/**` and `examples/**`, with a lit control) found zero instances, so there is no in-repo file to name. --> |
| 8 | + |
| 9 | +**BREAKING** accept-set narrowing on `FlowSchema` — a flow whose `edges[]` |
| 10 | +carries two edges with the same `id` is now **refused at parse time** — by |
| 11 | +`FlowSchema.parse` / `safeParse`, `defineFlow`, and every door that validates a |
| 12 | +flow through the schema (`objectstack validate`, the runtime publish gate, a |
| 13 | +stack's `flows[]`) — where it used to parse on green. Shipped as `minor` under |
| 14 | +the repo's launch-window convention for breaking changes. Maintainer ruling |
| 15 | +2026-09-05 on #14964 (director decision batch #40, verbatim 「同意」): option |
| 16 | +A — an `error`, not a `warning`; no opt-out, no transition window. |
| 17 | + |
| 18 | +Every reader of an edge id assumes the ids in a flow are unique — a designer, |
| 19 | +a BPMN export, a flow diff, any traversal that dedupes by id — and nothing |
| 20 | +enforced it. A real duplicate (`id: 'e20'` on two edges of one flow) shipped |
| 21 | +through two releases of green CI in a downstream app and was inert only |
| 22 | +because the engine keys out-edges by `source`, never by `id`: the collision is |
| 23 | +invisible until something keys on ids, and then silently wrong rather than |
| 24 | +loudly broken. The id space is hand-authored, so the next author picking a |
| 25 | +"free" id from the sequence had no way to know it was taken. |
| 26 | + |
| 27 | +**What changes** (`packages/spec/src/automation/flow.zod.ts`): a `superRefine` |
| 28 | +on the flow's `edges[]`. Each later occurrence of an already-declared id raises |
| 29 | +one `custom` issue, anchored at `edges[N].id` of the *later* edge and naming |
| 30 | +both positions, so the formatted error points at the edge to renumber: |
| 31 | + |
| 32 | +```text |
| 33 | +✗ edges.7.id: Duplicate edge id `e20` — `edges[7]` reuses the id already declared by `edges[3]`; every edge id in a flow must be unique. Renumber one of them: … |
| 34 | +``` |
| 35 | + |
| 36 | +**What does NOT change:** `edges[].id` keeps its name, type and describe; the |
| 37 | +node vocabulary, the edge `type` enum and every other refusal are untouched; |
| 38 | +a flow with unique edge ids (or no edges) parses exactly as before. Node ids |
| 39 | +are not covered by this change. |
| 40 | + |
| 41 | +The shape that is refused, and what the author does about it — a two-edge |
| 42 | +excerpt, the later edge renumbered: |
| 43 | + |
| 44 | +```ts |
| 45 | +// before — parsed on green, both edges keyed 'e20' |
| 46 | +edges: [ |
| 47 | + { id: 'e20', source: 'qualify', target: 'convert' }, |
| 48 | + { id: 'e20', source: 'convert', target: 'end' }, |
| 49 | +] |
| 50 | + |
| 51 | +// after — refused at parse (edges.1.id: Duplicate edge id `e20` …); renumber the later one: |
| 52 | +edges: [ |
| 53 | + { id: 'e20', source: 'qualify', target: 'convert' }, |
| 54 | + { id: 'e21', source: 'convert', target: 'end' }, |
| 55 | +] |
| 56 | +``` |
| 57 | + |
| 58 | +**Remedy.** Renumber the later edge to an id no other edge in that flow |
| 59 | +carries; nothing else in the flow needs to move. The census over this |
| 60 | +repository found no flow to migrate, so this is a release note, not a |
| 61 | +migration: no shipped example, fixture or seed in `packages/**` or |
| 62 | +`examples/**` declares a duplicate edge id, and the pinned objectui tree |
| 63 | +carries none in its authored flows. The one known downstream instance was |
| 64 | +renumbered before this change (hotcrm PR #1571). |
0 commit comments