finos / finos/architecture-as-code
Nest decision boxes and containers in pattern visualisation (follow-up to #2859)
- Dominant language
- TypeScript
- Stars
- 399
- Forks
- 138
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 38
Description
## Summary
When the pattern visualiser draws a diagram, every node gets exactly one parent box. A node can sit in a container (`deployed-in` / `composed-of`) or in a decision box, never in a decision box that is itself inside a container. That single-parent rule is why several ordinary patterns draw incorrectly, and why a decision can disappear from the diagram without any warning.
This issue replaces the rule.
## Current behaviour
Verified by running `parsePatternData` against `main`. None of it is pinned by a test.
| Shape | What draws |
|---|---|
| A decision whose candidates share a container | The decision box takes the candidates. The container draws **empty** beside it |
| A decision between containers | The box draws empty; the containers sit outside it |
| Two decisions over separate choices | Correct. Two boxes, both prompts |
| Two decisions naming the same candidate | **One** box. The second decision draws nothing — no box, no prompt |
| One decision with two option blocks | Two boxes, but only the **first** carries the prompt |
| A decision naming a node the pattern does not declare | Nothing, silently |
| A `oneOf` of relationships | Both edges draw. No box, so the choice is invisible |
`patternTransformer.ts` resolves a parent in one line, decision first:
```ts
const parentId = node.decisionGroupId || parentMap.get(node.uniqueId);
```
#2932 never merged, so none of its interim behaviour exists. There is no empty-box suppression in the file, and container precedence was never shipped.
## The two shapes to support
**A container holds a decision.** The candidates are leaf nodes sharing one container. The decision box nests inside that container: container, then decision box, then candidates.
**A decision chooses between containers.** The candidates are themselves containers. The containers nest inside the decision box: decision box, then container, then contents. This is the natural way to author "choose which subsystem".
## Also in scope
**A candidate named by two decisions.** A node has one parent, so one box cannot hold it twice. Today the first decision claims the group and the second is dropped entirely:
```ts
if (groupId && !groupOptionsMap.has(groupId)) { groupOptionsMap.set(groupId, meta); break; }
```
Any model this issue lands has to answer this case, because a single-parent tree cannot express it.
**Three faults that are not about nesting.** All three are "a decision that does not draw", and all three live in the code this issue rewrites.
| Fault | Cause |
|---|---|
| A decision with two option blocks keeps only the first prompt | `extractOptionsMetadata` returns on the first block that yields choices |
| A decision naming an unknown node draws nothing, silently | No group matches it, and nothing reports that |
| A `oneOf` of relationships draws no box | `extractRelationshipsFromPattern` sets a group id, but nothing downstream reads a relationship's `decisionGroupId` |
**Decision group ids keyed by array index.** A box is identified as `node-decision-` from its position in `prefixItems`. Insert an entry above it and every box below is renumbered, so anything remembering a box by id follows the wrong box. An `items` catalogue already had to sidestep this with a fixed name, `node-decision-items`.
## What changed under #2859
`items` catalogues now draw (PR #3110). That did not change the parenting model, but it makes one shape reachable that was not before: a catalogue node placed in a container. Verified on that branch:
```
decisionGroup:node-decision-items<-ROOT group:host<-ROOT
custom:cache<-node-decision-items custom:queue<-node-decision-items
```
So the container draws empty, exactly as the `prefixItems` case does. Whatever model this issue lands must cover catalogue-derived groups as well as `prefixItems` ones.
## Design sketch
`createReactFlowNodes` already receives the three inputs a correct answer needs: each node's container parent (`parentMap`), its decision-group membership (`decisionGroupId`), and whether it is itself a container (`containerNodeIds`). Compute one "where does this group belong" answer per decision group, then assign parents top-down — container, then decision box, then leaves.
`OptionsMetadata` should carry the decision's `unique-id`. `shared` already returns `optionId` and the UI type drops it, which is why a decision is matched to a group today by guessing from its first candidate.
For layout, `applyPatternLayout` already sizes nested groups innermost-first via `sortContainersDeepestFirst`. Extend it to treat a decision box the same when it has a parent or has group children, rather than adding a second layout pass.
Consider a validation rule flagging a decision whose candidates neither all share one container nor are all containers themselves, so the visualiser only has the two clean shapes to draw.
## Open questions
| Question | Why it blocks |
|---|---|
| Support both nesting directions, or only decision-between-containers? | Decides how much of the parenting rewrite is needed |
| How should a candidate named by two decisions draw? | A box cannot hold a node twice. The alternative is to reject the pattern |
| Is inconsistent container membership an error, a warning, or drawn flat? | Decides whether the renderer must cope with a third shape |
## Notes
- Pattern visualisation only. Architectures are drawn by `calmTransformer.ts`, which has no decision-group concept because the choices are already resolved.
- `patternTransformer.test.ts` pins none of the behaviour in the table above.
- `test_fixtures/decision-agreement/` holds a fixture contract asserted by both `shared` and `calm-hub-ui`. Keep both sides passing.
- Stacks after #3110, which renders `items` catalogues.
Contributor guide
Assessment
This issue has not been assessed yet.