finos / finos/architecture-as-code

Nest decision boxes and containers in pattern visualisation (follow-up to #2859)

Open
#2,933 1 comment 0 reactions 1 assignee Claimed by @YoofiTT96 View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.