Summarising a diagram¶
A complexity finding says a diagram is too big. A split plan says where to cut it. This page explains how a plan is found, what it promises, and what it leaves to the author.
Why a split plan, not a fix¶
Splitting a diagram changes what it says, so no complexity rule has a fixer, per MJL-0004. A fold on its own would be a smaller diagram with less in it. That is a summary that drops what it folds, and a reader could not tell what went missing.
A split plan keeps everything. It has two halves:
- An overview keeps the diagram's shape, with some sets of nodes folded into one group node each.
- One detail diagram per group shows that group's members and every edge between them.
A dashed stub in a detail stands for each outside node an edge reaches, or for a whole group an edge comes in from. Each outside node or group gets one stub. In a flowchart, a detail draws its stubs for two or more members of one group inside one box. The box shows the label the overview shows for that group.
Each stub inside shows its member's own label, as Zuul edge proxy inside Edge tier (+2), per MJL-0037.
A group standing for one author region whole is boxed as that region, so an edge to the region meets the box.
A detail draws a box only while it still meets the target.
Edges of one kind one way between a group's stub and one member are drawn once, keeping only the label they all share.
The group's own detail draws each of them to that member, so nothing is lost.
When edges join two groups both ways, one detail draws the other group whole and the other draws its members.
The detail split again draws the other's members, when the other's detail fits, per MJL-0035.
A stub in a nested overview then names the member a reader is sent to, as INVOICE_LINE (in INVOICE (+5)).
A stub for a member of a group names that group by the label the overview shows it with. A stub for the member the group is named after shows its own label alone, since the group's label leads with it. So a reader finds the group by its text, per MJL-0036. Otherwise the detail needing stubs for fewer of the other's members draws them, so no detail shows a group whole beside its members.
Edges may run one way only from a single member into several members of another group. The target's detail then draws them from one stub for that member, unless the source's detail is split again. A target whose detail is split again draws them too, per MJL-0038. The source's detail draws the target as one stub, per MJL-0036.
More generally, the target draws edges joining two groups one way when it needs fewer stubs. That is, stubs for fewer of the source's members than the source needs of its own. With as many, two or more, it draws them when its detail meets fewer outside nodes than the source's. It does so only while its detail still meets the target, per MJL-0037.
Together the overview and the details recover every original node and edge.
The plan is still a recommendation that a human applies, per MJL-0025. --split-plan prints it, the node count and score rules end their remediation with it, and nothing writes it to a file.
Each plan first says why it was offered, per MJL-0039.
It names the size the rules measured, rows counted, the split target, and which measure is over.
A diagram over only for the rows it draws, which meets the target with its rows dropped from the overview, folds little more.
An erDiagram folds only its closed aggregates, whose other members relate to nothing outside them, per MJL-0040.
Its plan says so, and its rows move to row diagrams.
The shape follows Shneiderman's mantra: overview first, zoom and filter, then details on demand. It also follows the limits of working memory. Miller (1956) put the span near seven items, and Cowan (2001) revised it down to about four chunks. A group node is a chunk: one name the reader holds in place of its members. Moody (2009) names modularisation and hierarchy as the tools for managing a notation's complexity, and a plan uses both.
The graph theory¶
Summarising a graph is an old problem with a large literature. The plan borrows a handful of its ideas, chosen because each is deterministic and each makes a claim that can be checked.
Quotient graphs¶
Partition the nodes into blocks, and draw one node per block. Two blocks are joined when some original edge runs between their members. The result is a quotient graph, and every overview is one.
Tian, Hankins and Patel (2008) build summaries this way, grouping nodes that agree on attributes and relationships. Navlakha, Rastogi and Shrivastava (2008) add corrections, a list of edges to add or remove, so the summary can be expanded back exactly. A detail diagram plays the part of a correction: it holds what the overview folded.
The strongest blocks are the author's own. A flowchart subgraph, a composite state and a class namespace each say "these belong together" in the author's words. Folding one gives a group node that carries the author's label.
flowchart LR
subgraph before [Before]
O1[Order] --> P1
subgraph billing [Billing]
P1[Payment] --> I1[Invoice]
end
I1 --> E1[Email]
end
subgraph after [After]
O2[Order] --> B2[["Billing (+2)"]]
B2 --> E2[Email]
end
Twins and modules¶
Two nodes are twins when they have the same neighbours, joined by edges with the same labels and arrows. One node can stand for both, and nothing that reached one or the other changes. Twins are the smallest case of a module: a set of nodes that every outside node treats alike. Modular decomposition finds every module of a graph, nested as a tree.
Royer and colleagues (2008) draw protein networks as power graphs, where a module becomes one power node. They report that this removes a large share of the edges without losing any. A bundle of sibling leaves is the simplest module of all: pendants of one node, each with the same kind of edge.
flowchart LR
subgraph before [Before]
S1[Shipping] --> M1[Email]
S1 --> T1[Text message]
S1 --> P1[Push alert]
end
subgraph after [After]
S2[Shipping] --> M2[["Email (+2)"]]
end
A large set of twins is cut into runs, each small enough for its detail to meet the target.
The twins are first put in natural order of their labels, so each run reads as an alphabetical volume of an index.
A gateway's twenty services become runs such as accounts-service … feature-flag-service (+9).
Twin chains are the exception, per MJL-0038.
They are lanes, such as extracts each feeding the bronze table it lands in, and nothing in their labels sorts them.
So they fold whole, every lane in one step.
When their detail does not fit, it is split again, and the lanes fold in runs in it, per MJL-0042.
Lanes whose labels lead with the same words, as Pull billing events and Pull refund events, fold as one group when each such set fits.
Else the lanes fold in natural order of their labels, as many as fit, cut evenly.
So an ETL pipeline's lanes read Extract → bronze (+8) and Extract/Ingest → bronze (+6) in a detail of four nodes, where they read seven groups of one lane each.
A lane in no run folds alone.
Only when folding them whole gives no plan are they cut into runs in the overview.
Chains and stages¶
A run of nodes, each with one edge in and one edge out, is a chain. Folding part of a chain keeps every path through it, so nothing new becomes reachable. A long chain is never folded into one node, because a reader would lose how many steps it has. It is cut into stages, each small enough for its detail diagram to meet the target.
flowchart LR
subgraph before [Before]
A1[Receive] --> B1[Check] --> C1[Pack] --> D1[Label] --> E1[Load] --> F1[Deliver]
end
subgraph after [After]
A2[Receive] --> B2[["Check (+1)"]] --> D2[["Label (+1)"]] --> F2[Deliver]
end
Dominators and decision regions¶
A decision is a step whose branches split the flow, such as a diamond or an edge labelled Yes.
Its branches only make sense together, so a plan folds a decision with everything between it and the point where its branches rejoin.
A node d dominates a node v when every path from the flow's entry to v passes through d.
Run on the reversed graph, from where the flow leaves, the same idea gives post-dominators.
The immediate post-dominator of a decision is the nearest node every one of its branches must reach, the rejoin point.
The plan computes both with the iterative algorithm of Cooper, Harvey and Kennedy (2001).
It starts from a virtual entry joined to every node nothing leads into, and works towards a virtual exit.
The region from a decision to its rejoin point is single-entry single-exit (SESE).
Every edge into it enters the decision, and every edge out of it reaches the rejoin point.
Johnson, Pearson and Pingali (1994) build the program structure tree from these regions, nested one inside another.
A branch that ends the flow, such as Decline, is an outcome of the decision, not a way out of its region.
So post-dominators are computed without the nodes where the flow ends, and those nodes fold inside the region.
Folding a SESE region whole invents no path.
Everything that reached the decision reaches the group, and the group reaches only what the rejoin point reaches.
So the fold belongs to T2, the lossless tier.
Regions fold innermost first, and each group is named after its decision.
The branch labels stay in the detail diagram, and a chain stage may pass through a folded region.
flowchart LR
subgraph before [Before]
A1[Apply] --> D1{Score ok?}
D1 -->|No| R1([Decline])
D1 -->|Yes| O1[Offer]
D1 -->|Maybe| M1[Review] --> O1
end
subgraph after [After]
A2[Apply] --> D2[["Score ok? (+2)"]] --> O2[Offer]
end
A region with a second entry, or a branch leaving through a second node, is not SESE, so its decision stays protected.
Aggregates¶
An erDiagram says who owns what with identifying relationships, drawn as a solid line.
An entity is owned by its single identifying parent.
With none, an entity with one relationship is owned by the entity it depends on, the one with exactly one at its end.
Owners chain to a root, and an aggregate is a root with everything it owns.
This is the aggregate of domain-driven design, read from the author's own notation.
T4 folds the largest aggregate whose detail fits, named after its root, so the overview's edges run between roots.
When none fits, it may fold any owner's aggregate and split the detail again.
An aggregate folded over the target is never folded again into its owner's, so no one group takes most of the diagram.
A junction, an entity identified by entities in two aggregates, such as a review joining a product and a customer, is owned by neither.
It stays in the overview, since folding it into one side would draw the other side's relationship onto the wrong root.
An erDiagram never merges two relationship labels, so a group can draw two lines to one entity where the original draws one.
An address chain folded with the STORE it locates draws CUSTOMER twice, once for "lives at" and once for "registers".
So an aggregate folds only where it draws no such pair of lines, per MJL-0034.
Lines the original itself draws side by side between two entities are kept.
A root whose whole aggregate would draw one is offered trimmed too.
Each owned part sharing an outside neighbour with the rest is cut away, so the address chain folds alone and STORE stays in view.
A trimmed aggregate folds only where it adds no new reachable pair.
flowchart LR
subgraph before [Before]
C1[Customer] -->|places| O1[Order] -->|contains| L1[Line]
P1[Product] -->|offered as| V1[Variant]
L1 -.->|sells| V1
end
subgraph after [After]
C2[["Customer (+2)"]] -.->|sells| P2[["Product (+1)"]]
end
Leaves and the k-core¶
A leaf is a node with one neighbour. Folding each leaf into the node it hangs from removes clutter but keeps the hub in view. Repeated, the folding peels the graph towards its 2-core, the part where every node has at least two neighbours. TopoLayout (Archambault, Munzner and Auber 2007) treats such trees as topological features, and collapses them before laying out the core.
flowchart LR
subgraph before [Before]
L1[Audit log] --> H1[Payments]
H1 --> K1[Card processor]
H1 --> R1[Ledger]
R1 --> Q1[Reports]
Q1 --> H1
end
subgraph after [After]
H2[["Payments (+2)"]] --> R2[Ledger]
R2 --> Q2[Reports]
Q2 --> H2
end
This fold is lossier than the ones above. Once a leaf sits inside the group of the node it hangs from, the overview may show the group reaching nodes the leaf never could.
Communities and modularity¶
When structure alone finds no more folds, the plan looks for communities: sets of nodes with more edges inside than out. Modularity scores a partition by how far its inside edges exceed what chance would give. Greedy agglomeration starts with every node alone, then repeatedly merges the pair that gains the most modularity. The plan merges two nodes an edge joins.
It also merges two nodes of one prefix family that share a neighbour, as two silver. tables fed by one quality suite.
They are one stage of a pipeline, which no edge joins.
So a suggested group is always connected, through its own edges or the neighbours its members share, and the contract checks it.
That is the method of Clauset, Newman and Moore (2004).
flowchart LR
subgraph before [Before]
A1[Cart] --> B1[Checkout]
B1 --> C1[Basket total]
C1 --> A1
B1 --> D1[Payment]
D1 --> E1[Refund]
E1 --> F1[Payment record]
F1 --> D1
end
subgraph after [After]
A2[["Checkout (+2)"]] --> D2[["Payment (+2)"]]
end
The plan uses greedy agglomeration because it is deterministic. Ties are broken by sorted node ids, so the same graph always gives the same plan. Louvain, the most popular modularity method, visits nodes in random order. Traag, Waltman and van Eck (2019) also showed it can return a community that is not connected.
Their Leiden method fixes that, but it still relies on randomness. Label propagation was rejected for the same reason: its result depends on update order, and it can oscillate. A merge never crosses an author region, so a suggested group never splits what the author grouped.
Cliques in a dense mesh¶
In a dense mesh nearly every pair of nodes is joined, so merging one pair says little.
A mesh counts as dense when its overview joins at least half of all node pairs.
There T6 also proposes cliques: sets of nodes that all talk to each other.
It finds the maximal cliques of the nodes not yet grouped with the algorithm of Bron and Kerbosch (1973).
It uses the pivot rule of Tomita, Tanaka and Takahashi (2006), so each maximal clique is found once. Nodes are taken in natural order, and ties go to the first. At most 256 cliques are listed, so the same graph always gives the same cliques. Each clique is cut into runs whose detail meets the target.
A run ranks by the modularity gained by merging it whole, the sum of its pairs' gains, so a larger clique goes first.
A diagram that is a dense mesh from the start gets no plan. It counts as one when it joins at least half of all node pairs and more pairs than twice its nodes. Its nodes then average more than four links. Folding such a mesh leaves a mesh, since its groups are joined to each other too, and no fold makes it readable.
So the plan is refused with the reason "dense mesh: a matrix or table reads better than a diagram". The same check runs on each plan's overview, so a plan never offers one, per MJL-0030.
The refusal prints the table it names, per MJL-0034. --split-plan prints the diagram as a markdown adjacency matrix after the reason, a row and a column per node.
Each cell holds every edge between its row and column, drawn from the row's side.
→runs from the row to the column.←runs from the column to the row.↔runs both ways.●has no direction.
A label follows in italics and any connector but a plain arrow or line in code. A line before the table names the pairs no edge joins, which in a near-complete mesh is what a reader looks for. The matrix is built from the parsed graph alone, and a test reads it back through mdast to find every node and edge.
Measures that did not help¶
Bridges and betweenness are the usual tools for finding where a network divides. Neither helped on diagrams. Most architecture diagrams are close to trees, and in a tree every edge is a bridge. On the shop architecture from Measuring complexity, most edges are bridges, so a bridge says nothing about where to cut. Betweenness peaks at the hubs, which are the nodes an overview should keep in view, not fold away.
The tier ladder¶
The plan applies its folds in tiers, least lossy first.
A diagram with author regions climbs T3 first, since a reader finds the author's own boundaries first.
In T3, a region folds in one step with every region that echoes it.
An echo is a sibling of the same shape holding regions of its own, or the same-labelled child of an echoing region.
A region's shape is how many nodes it holds and each child region's label and shape.
So two cloud regions with the same subnets fold to the same depth.
Another group may be named after a region of the same label.
A group then takes the label of the region around it in brackets, as Public subnets (us-east-1) (+2).
A plan may split a detail again with new reachable pairs. It is then walked again holding regions to details that fit, first folding only regions that add no new reachable pair. That plan is offered when it declares fewer new reachable pairs in all. A region folds whole however much of a diagram it holds, its detail split again when it does not fit, per MJL-0035.
So a compiler's Ast namespace folds as one group, its subtrees folded inside its detail.
When that plan fails, it climbs in the order below.
Within a tier, it applies the fold that saves the most score per node hidden, then measures the overview again.
In T4, of two aggregates that save as much, the one leaving fewer edges between overview nodes goes first.
In T4, T5 and T6, some folds come within 5% of the best one's saving, or in T6 its gain.
Of those, the one leaving the fewest parallel edges goes first.
A parallel edge is an overview edge beside another between the same two nodes, at least one of them a group.
A chain folds in even stages and a set of twins in even runs, all in one step.
It stops at the first overview that meets the preset's nodeTarget and vcsTarget.
No fold may leave the overview with fewer than four nodes, unless the diagram has fewer than eight.
Once the target is met, the plan evens out what it folded.
It does so only while the overview keeps at least 60% of nodeTarget, rounded up, so an overview lands near its target.
If a detail then fails its target, the evening goes on below that share, down to the fewest nodes a fold may leave.
A greedy walk can also overshoot below that share, when an early fold leaves a later one needed.
The plan then undoes the group whose undoing gives back the most nodes and still meets every target, until the share is met.
A T2 group stays, since undoing one run would leave its pattern half folded.
In T2 it folds the remaining decision regions, so every decision reads alike.
In T3 it folds the sibling regions of the ones folded, so sibling regions read alike.
In T4 it folds more aggregates when the erDiagram overview is tangled, meeting a cycle for every four nodes.
Each such fold takes away an overview edge per entity it hides and adds no new reachable pair, so only tight aggregates fold.
A diagram that meets the target by dropping rows alone folds only its closed aggregates, tangled or not, per MJL-0040.
In a closed aggregate one entity holds every relationship that crosses its edge, as ROOM with BUILDING.
So the overview still draws each relationship at the entity it joins.
An aggregate whose other members relate outside it stays in view, so a university schema keeps ASSESSMENT beside SECTION.
In T5 and T6 it gathers what is left open into groups of three or more, no larger than the largest.
When every label leads with an id naming a family, such as PLAT-1201, T6 first folds each family whole, hubs included.
A family too large for one detail folds as even contiguous runs, each connected. Once a family folds, the evening takes only families, below the 60% share, and a family's group is never undone. A detail diagram must meet the same targets, and one that does not is split once more. A plan names the last tier that changed it.
T5 and T6 follow five rules, so the overview still tells the diagram's story:
- Hubs stay in view. A hub is a node of degree at least four and at least the 90th percentile degree.
It keeps its own overview node, and the leaves hanging from it fold beside it as one group.
An
erDiagramnever merges two relationship labels, so there its leaves stay open. - Shared terminals stay in view. A shared terminal is a flowchart or
stateDiagramnode that at least three other nodes lead into and that ends the flow, by directed edges. It leads nowhere, or everything it leads to leads nowhere. ANotify on-callstep every failure ends at is one, and so is a finalRefundedstate. So is aCancelledstate that leads only toRefundedandVoided.
Folded into one branch's group, it would read as that branch's own end.
- A decision's branches stay apart. A decision is a node whose outgoing edges carry two labels or more, in a flowchart or a stateDiagram.
Two targets of its differently labelled branches never share a group, unless one reaches the other, or both reach a common member, inside the group.
- A group keeps to one prefix family. A prefix family is the dotted prefix leading a label, such as bronze..
Failing that, it is the letters leading an id-led label, such as PLAT-.
Failing that, it is the letters and underscore leading an id, such as br_.
Failing that, a label leading with a bare id takes a status stem it shares with another.
So 10008 Ready for Release and 10009 Released share releas.
No suggested group holds members of two families.
A chain stage may, so an extract folds with the bronze table it lands in as one lane, and lanes alike fold as twins.
In T6, a pair within one family goes before any other pair, joined by an edge or by a neighbour they share.
Of two such pairs alike in gain and label, the one sharing more overview neighbours goes first.
So a layer folds in step with the layer beside it.
- Every group hides two nodes or more. Once the target is met, a group hiding one node is undone when the overview and every detail still meet the target without it.
Otherwise it is merged into a neighbouring node or suggested group.
One that can be neither stays.
A decision region from T2 hiding one node is undone the same way.
It is never merged, since a merged region would no longer be a region.
When the target cannot be met under these rules, the plan is walked again with prefix families mixed. When that fails, it is walked with hubs and branches folded too, while shared terminals stay in view. That walk first keeps prefix families apart, and only then mixes them. When that fails too, it is walked without the first four rules, and the last still holds.
As a last resort, the plan is walked once more with groups that hide one node allowed.
A decision node drawn as a diamond, a fork, a join and a choice are protected on every walk.
Each folds only with a whole decision region or author region around it.
An erDiagram junction between two aggregates is kept in view under the first two rules, like a hub.
A plan may group only runs of one hub's sibling leaves, each named only by a range.
It is refused when those groups hold more than half the diagram.
Nothing the author drew sorts those leaves, so the groups would be alphabetical buckets, and a list reads better. --split-plan prints that list after the reason, per MJL-0034.
It names the node with the most links, then each of its links by way, label and connector, each leaf an item.
Every other edge follows under its source, and a node no edge touches follows last.
The declared loss is the number of pairs of overview nodes where one reaches the other in the overview but not in the original. The plan reports it after each tier.
| Tier | What it folds | What it may lose |
|---|---|---|
T1 |
No nodes. It leaves attribute rows out of the overview. | Nothing. A detail draws its members' rows when they fit, and row diagrams draw the rest. No reachable pair. |
T2 |
Twins, bundles of sibling leaves, chain stages, and decision regions, innermost first | Nothing. The declared loss after T2 must be zero. |
T3 |
Author regions, innermost first: subgraphs, composite states and namespaces | New reachable pairs between nodes folded into one region |
T4 |
erDiagram aggregates: the largest aggregate that fits, named after its root, or after its most crossed member when the root is a lookup. A junction stays out of both sides. |
New reachable pairs. Cardinalities are never merged. |
T5 |
Leaves into the node they hang from, repeated, or beside it as one group when it is a hub | New reachable pairs through the group |
T6 |
Suggested groups from greedy modularity, over pairs an edge joins, within one prefix family first, with pairs of one family sharing a neighbour too, alike labels breaking ties, and tight cliques once the overview is dense | New reachable pairs, and groups the author may not recognise |
What "meaning is preserved" means¶
A plan is offered only when a contract passes, and src/split/contract.ts checks it on every plan.
The contract is what this page means by "meaning is preserved".
- Sound. Every original node is in exactly one overview node. A suggested group is connected. Two members are joined by an edge, or by an author region the group folds whole. They are also joined by a neighbour they share, when both name one prefix family or both are leaves.
Each overview edge stands for original edges, in the same direction, and none is invented.
The one exception is an undirected edge, below.
- Sound attributes. An overview edge keeps a label or an arrow kind only when every edge it stands for agrees.
- Recoverable. The overview and the details give back exactly the original nodes and edges, with their labels and kinds.
Each edge between two groups is drawn exactly in one of the two groups' details.
The other detail draws it once for every edge between the other group and that member that draws alike.
- Labels from data. A group's label is an author label or a member's label, followed by a count of the rest, such as Web (+3).
A family, lane or fan label, recomputed from the members, counts every member, such as gold.* (+5).
A pair label, recomputed from a group's two members, shows the other's label in place of the count, such as ROOM (+BUILDING).
A list label does so for a group of three, the other two in natural order, such as ADDRESS (+CITY, COUNTRY).
A group of leaves takes one of any size within 80 characters.
A range, family or lane label still comes first.
A span label names a flowchart group by the member its flow enters at and the one it leaves from.
It counts the members neither names, such as git push to main → tsc --noEmit (+4).
A long label is cut in its middle with an ellipsis, and a label another node shows adds the group's id.
- Honest ranges. A range label, such as DATA-0381 … DATA-0383 (+1), is the group's first and last member labels in natural order.
Its count is of the members neither names.
No member of another group has a label from the first to the last.
- One reading per stub. A stub's text is read by every form a plan writes, against every label the diagram and its groups show, and has exactly one reading.
A text that could name two things adds the id it stands for, as often as it takes.
- Stub forms. A stub for a kept node shows that node's label. Refunded (in Delivered (+3)) is the member Refunded inside the group the overview shows as Delivered (+3). Delivered is the member that group is named after, and the group's label leads with it.
A stub for a member of a group with a pair or list label shows its own label alone.
So does a stub for either end a span label names, and for the first or the last member a range label names. ORDER (+6) from PAYMENT is the whole group, drawn from its member PAYMENT, and PAYMENT must draw every edge the stub draws.
A plan no longer writes it, since no detail draws a group whole beside its members.
A box showing Edge tier (+2) stands for the region that group folds whole.
A group folding no one region holding every member is boxed as the group, and its box draws no edge.
A stub inside it showing Zuul edge proxy is the one member of that group with that label.
- Undirected edges written both ways. Take a link with no arrowhead or end marker, written both ways between two overview nodes, at least one a group.
When both ways agree on kind and label, the overview draws it once.
Each detail draws each one as written, and the plan declares how many the overview draws the other way.
- Merged start and end transitions. The overview draws a start or end marker's transitions into one group as one transition onto the group, labelled only when they all agree.
The group's detail draws each as written, and the plan declares how many were merged.
- Protected nodes. A decision node, a fork, a join, a choice and a composite state stay visible, unless a whole region around them folds.
That region is an author region, or a decision region that is single-entry single-exit in the original graph, every node of which the group holds.
So does a state a composite's start or end marker reaches, unless that composite folds whole.
- Declared loss. The new reachable pairs the plan reports are recounted, and must be zero after T2.
- Order independent. Every tie is broken by sorted ids, so reordering the statements of a diagram gives the same plan.
- Round trip. Every emitted diagram parses again with the shipped parsers, and its measured size meets the target.
The contract does not promise everything a reader might assume.
- Groups may not match intent. A plan groups by structure and label stems, not by what the author meant.
- The overview can overstate reachability. That loss is declared, not prevented, from
T3onwards. - A folded edge can lose its label. When the edges a group edge stands for disagree, it carries none, and the detail keeps each one.
- Layout is not kept. Mermaid lays out each emitted diagram afresh.
Label language¶
Labels feed grouping in two places.
A group without an author label is named after one of its members.
In a flowchart or stateDiagram, a chain-like group is named after its entry, the member no other member leads into.
A group is chain-like when its members form one directed path, so a pipeline's first group reads git push to main (+5), not its exit.
Twin chains, several disjoint paths, are named after the entry the flow reaches first, then the first by label.
They are never named by a range across two stages.
Any other suggested group, from T5 or T6, is named after the member on its main path.
That is the member with the most edges crossing the group's boundary, a start or end transition counted as one.
A tie goes to the member the flow reaches first from where it enters the diagram.
Any other group is named after its best-connected member.
An aggregate is named after its root, unless the root is a lookup, every edge of which joins one other member.
Then it is named after the member with the most edges crossing its boundary, so Sakila's LANGUAGE and FILM read FILM (+1).
Some groups hold members no single label names.
These are twins, sibling leaves, and members whose labels all lead with an id, such as S10004 or 10011 Wont Do.
Such a group is named by a range: its first and last member labels in natural order, joined by a spaced ellipsis.
An example is 10002 Backlog … 10011 Wont Do (+1), where the count is of the members neither end names.
A range is offered for three members or more, with no markup, within 40 characters. It must not mislead a reader. So it is refused when a member of another group has a label from its first to its last. A reader would look for that member here.
An author region's label and a decision region's question still come first.
An id-led group with no honest range is named by its first label in natural order, so 10002 Backlog (+2) rather than a side state.
Some groups are named by what every member shares, per MJL-0036.
A group holding a whole dotted family is named by it, as gold.* (+5) for five gold tables.
It holds three members or more, and no node outside it leads with that namespace, so halves of a layer keep a member's name.
Twin chains are named by their stages, as Extract → bronze (+8) for four extracts each feeding the bronze table it lands in.
Each place along the chains is named by the namespace every label there leads with.
Failing that, it is named by the first word they all lead with, and failing that it shows an ellipsis.
Failing both, two first words name the place, joined by a slash, per MJL-0038.
So lanes led by extracts and an ingest read Extract/Ingest → bronze (+14), and lanes whose sources share no word read … → bronze (+6).
A group holding every leaf one overview node leads to is named by every member when the list fits 80 characters, per MJL-0040.
So the APIs Checkout calls read Carrier quote API (+ECB rates feed, Payment provider API, SMTP relay).
Otherwise it is named by that node, as Order service → … (+5), and leaves leading into one node read … → and its name.
A group of two members names both, as ROOM (+BUILDING), the other's label in place of the count.
Neither label may hold a bracket or markup, and the label holds 40 characters at most.
Groups of two folded alike take it only when every one of them can, and a stub for either member shows its own label.
Both labels count every member, and come after an author region's label and a decision region's question, before a range or a member's label.
A group of three names every member the same way, the other two in natural order, as ADDRESS (+CITY, COUNTRY), per MJL-0039.
Neither other label may hold a comma, and a group a range, family or lane label names keeps that label. A flowchart group of three or more may be entered at one member and left from another. It folds no region and has no decision or aggregate head. The entry is the one member every edge from outside reaches, else the one member no edge reaches, and the exit likewise.
Such a group is named by both, as git push to main → tsc --noEmit (+4) for a run of setup steps, counting the members neither names.
That name comes after a range and a fan, before a member's label, and a stub for either end shows its own label.
In T6, a pair within one prefix family goes before any other.
Under the strict rules, a pair across two families is never proposed.
A family is a dotted prefix leading a label, such as bronze., or the letters leading an id-led label, such as PLAT-.
Failing that, it is letters and an underscore leading an id, such as br_.
Failing that, for a label leading with a bare id, it is the one status stem the label shares with another such label.
So 10008 Ready for Release and 10009 Released form one family, and no other status of that workflow joins it.
When families are mixed, a pair across two still loses a tie between equal gains.
Among the rest, the pair with alike labels goes first.
Two nodes no edge joins pair only when they name one family and share a neighbour, however alike their labels.
Labels are compared by stems.
Each label is split into words by the platform's word segmenter.
Each word is reduced to its stem by the stemmer package, which implements Porter's algorithm.
Two labels score by the Jaccard overlap of their stems.
A role word, such as service, queue or DB, weighs less than a domain word.
So Order service and Orders DB are close, and Order service and Payment service are not.
An id-like label, such as A or N12, says nothing about meaning, so it adds nothing.
Compromise, a maintained natural-language library, was evaluated for the same job.
The sibling package jpai-prose-gates uses it to measure how well a paragraph's sentences hang together.
On diagram labels it gave the same groupings as the stemmer, for an install of about 4.4 MB, so it was not adopted.
Per diagram type¶
| Diagram type | Planned | Its regions | Notes |
|---|---|---|---|
flowchart, flowchart-elk, graph |
Yes | Subgraphs | A decision node folds only with its decision region. |
stateDiagram |
Yes | Composite states | A fork, a join and a choice fold only with a whole region. A state's description lines are carried with it. A diagram with concurrent regions is not split. |
erDiagram |
Yes | None | T4 folds aggregates by root, and keeps junctions out of both sides. Attribute rows are drawn in details and row diagrams, and any that fit in none are counted. |
classDiagram |
Yes | Namespaces | Members are drawn like attribute rows. A stub carries a <<stub>> annotation and a group a <<group>> one, since the grammar takes no dashed style. A lollipop interface is never folded: the overview hangs it from the group folding its class. |
sequenceDiagram |
No | None | The graph drops message order, so a fold would lose the story. The finding keeps its subgraph hint. |
Every other type is not planned, and its finding keeps the subgraph hint.
A worked example¶
The site build plans two diagrams under every preset, with the split code itself.
Each overview, detail, tier and loss below is what splitPlan returns, never typed by hand.
Each plan's ladder line lists the overview after each tier that changed it.
So every preset shows the climb to the tier that met its target.
The diagram¶
This shop architecture has more services than the one on Measuring complexity, and four author subgraphs. It scores 62.7.
Mermaid source
flowchart TD
CUST[Customer]
ADMIN[Administrator]
CDN[Content cache]
WAF[Web firewall]
subgraph web [Web]
UI[Storefront]
ADM[Admin console]
API[API gateway]
end
subgraph orders [Orders]
OS[Order service]
OQ[Order queue]
OW[Order worker]
end
subgraph billing [Billing]
PS[Payment service]
INV[Invoice service]
REF[Refund service]
end
subgraph shipping [Shipping]
SHS[Shipping service]
LBL[Label printer]
TRK[Tracking feed]
end
SS[Stock service]
WH[Warehouse feed]
RES[Reservation service]
DB[(Orders DB)]
PDB[(Payments DB)]
SDB[(Stock DB)]
CACHE[(Session cache)]
MAIL[Email]
SMS[Text message]
PUSH[Push alert]
PSP[Card processor]
CAR[Carrier]
LOG[Audit log]
CUST --> CDN
CDN --> WAF
WAF --> UI
ADMIN --> ADM
UI --> API
ADM --> API
API --> CACHE
API --> OS
OS --> OQ
OQ --> OW
OW --> PS
OW --> RES
OS --> DB
PS --> PDB
PS --> PSP
PS --> INV
REF --> PS
ADM --> REF
INV --> MAIL
RES --> SS
SS --> SDB
WH --> SS
OW --> SHS
SHS --> LBL
SHS --> CAR
CAR --> TRK
TRK --> SHS
SHS --> SMS
SHS --> PUSH
OS --> LOG
PS --> LOG
REF --> LOG
The diagram as drawn
flowchart TD
CUST[Customer]
ADMIN[Administrator]
CDN[Content cache]
WAF[Web firewall]
subgraph web [Web]
UI[Storefront]
ADM[Admin console]
API[API gateway]
end
subgraph orders [Orders]
OS[Order service]
OQ[Order queue]
OW[Order worker]
end
subgraph billing [Billing]
PS[Payment service]
INV[Invoice service]
REF[Refund service]
end
subgraph shipping [Shipping]
SHS[Shipping service]
LBL[Label printer]
TRK[Tracking feed]
end
SS[Stock service]
WH[Warehouse feed]
RES[Reservation service]
DB[(Orders DB)]
PDB[(Payments DB)]
SDB[(Stock DB)]
CACHE[(Session cache)]
MAIL[Email]
SMS[Text message]
PUSH[Push alert]
PSP[Card processor]
CAR[Carrier]
LOG[Audit log]
CUST --> CDN
CDN --> WAF
WAF --> UI
ADMIN --> ADM
UI --> API
ADM --> API
API --> CACHE
API --> OS
OS --> OQ
OQ --> OW
OW --> PS
OW --> RES
OS --> DB
PS --> PDB
PS --> PSP
PS --> INV
REF --> PS
ADM --> REF
INV --> MAIL
RES --> SS
SS --> SDB
WH --> SS
OW --> SHS
SHS --> LBL
SHS --> CAR
CAR --> TRK
TRK --> SHS
SHS --> SMS
SHS --> PUSH
OS --> LOG
PS --> LOG
REF --> LOG
What the parser returned: 29 nodes, 32 edges, 4 subgraphs
depth = max(subgraph nesting 1, tree depth 0) = 1
score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
= (29 + 0.5 × 32 + 3 × 4) × (1 + 0.1 × 1)
= (29 + 16 + 12) × 1.1
= 57 × 1.1
= 62.7
Each preset's plan¶
A smaller target needs more folding, so it climbs further up the ladder.
| Preset | nodeTarget / vcsTarget |
Tier | Overview nodes / score | Details | New reachable pairs |
|---|---|---|---|---|---|
low-density |
10 / 20 | T5 | 8 / 12.5 | 6 | 0 |
medium-density |
15 / 30 | T5 | 9 / 14 | 6 | 0 |
high-density |
25 / 40 | T3 | 21 / 32.5 | 4 | 0 |
Low density¶
Under low-density, the plan selects T5.
Folding author regions in T3, then a chain stage and twins in T2, did not reach this target.
So in T5 leaves fold into the nodes they hang from.
low-density targets 10 nodes and a score of 20.
Overview source
flowchart TB
g3[["Customer (+Content cache, Web firewall)"]]
g1[["Admin console (+4)"]]
g4[["Refund service (+5)"]]
g5[["Orders (+3)"]]
LOG["Audit log"]
DB[("Orders DB")]
g2[["Shipping service (+5)"]]
g6[["Reservation service → Stock DB (+2)"]]
g3 --> g1
g1 --> g4
g1 --> g5
g4 --> LOG
g5 --> g4
g5 --> LOG
g5 --> DB
g5 --> g2
g5 --> g6
The overview as drawn
flowchart TB
g3[["Customer (+Content cache, Web firewall)"]]
g1[["Admin console (+4)"]]
g4[["Refund service (+5)"]]
g5[["Orders (+3)"]]
LOG["Audit log"]
DB[("Orders DB")]
g2[["Shipping service (+5)"]]
g6[["Reservation service → Stock DB (+2)"]]
g3 --> g1
g1 --> g4
g1 --> g5
g4 --> LOG
g5 --> g4
g5 --> LOG
g5 --> DB
g5 --> g2
g5 --> g6
What the parser returned for the overview: 8 nodes, 9 edges, 0 subgraphs
target: 10 nodes, score 20
tier: T5
overview: 8 nodes, score 12.5
groups: Admin console (+4) [T5, leaf fold]; Shipping service (+5) [T5, leaf fold]; Customer (+Content cache, Web firewall) [T5, leaf fold]; Refund service (+5) [T5, leaf fold]; Orders (+3) [T3, author region]; Reservation service → Stock DB (+2) [T5, leaf fold]
declared loss: 0 new reachable pairs (after each tier: T1 0, T3 0, T2 0, T4 0, T5 0)
rows dropped: 0
ladder: T1 29 nodes, score 62.7; then T3 21 nodes, score 32.5; then T2 18 nodes, score 28; then T5 8 nodes, score 12.5
Detail of Admin console (+4): 8 nodes, score 15.95
flowchart TB
ADMIN["Administrator"]
s3(["Refund service"])
CACHE[("Session cache")]
s2(["Order service (in Orders (+3))"])
s1(["Customer (+Content cache, Web firewall)"])
subgraph web["Web"]
ADM["Admin console"]
API["API gateway"]
UI["Storefront"]
end
ADMIN --> ADM
ADM --> API
ADM --> s3
API --> CACHE
API --> s2
s1 --> UI
UI --> API
classDef stub stroke-dasharray:4
class s1,s2,s3 stub
Detail of Shipping service (+5): 7 nodes, score 14.85
flowchart TB
s1(["Orders (+3)"])
CAR["Carrier"]
PUSH["Push alert"]
SMS["Text message"]
subgraph shipping["Shipping"]
SHS["Shipping service"]
LBL["Label printer"]
TRK["Tracking feed"]
end
s1 --> SHS
SHS --> CAR
SHS --> LBL
SHS --> PUSH
SHS --> SMS
CAR --> TRK
TRK --> SHS
classDef stub stroke-dasharray:4
class s1 stub
Detail of Customer (+Content cache, Web firewall): 4 nodes, score 5.5
flowchart TB
CUST["Customer"]
CDN["Content cache"]
WAF["Web firewall"]
s1(["Storefront (in Admin console (+4))"])
CUST --> CDN
CDN --> WAF
WAF --> s1
classDef stub stroke-dasharray:4
class s1 stub
Detail of Refund service (+5): 9 nodes, score 18.15
flowchart TB
s1(["Admin console (+4)"])
s3(["Audit log"])
PDB[("Payments DB")]
PSP["Card processor"]
MAIL["Email"]
s2(["Orders (+3)"])
subgraph billing["Billing"]
REF["Refund service"]
PS["Payment service"]
INV["Invoice service"]
end
s1 --> REF
REF --> PS
REF --> s3
PS --> s3
PS --> INV
PS --> PDB
PS --> PSP
INV --> MAIL
s2 --> PS
classDef stub stroke-dasharray:4
class s1,s2,s3 stub
Detail of Orders (+3): 9 nodes, score 17.6
flowchart TB
s1(["Admin console (+4)"])
s2(["Orders DB"])
s3(["Audit log"])
s4(["Payment service (in Refund service (+5))"])
s5(["Reservation service"])
s6(["Shipping service"])
subgraph orders["Orders"]
OS["Order service"]
OQ["Order queue"]
OW["Order worker"]
end
s1 --> OS
OS --> OQ
OS --> s2
OS --> s3
OQ --> OW
OW --> s4
OW --> s5
OW --> s6
classDef stub stroke-dasharray:4
class s1,s2,s3,s4,s5,s6 stub
Detail of Reservation service → Stock DB (+2): 5 nodes, score 7
flowchart TB
WH["Warehouse feed"]
SS["Stock service"]
SDB[("Stock DB")]
s1(["Orders (+3)"])
RES["Reservation service"]
WH --> SS
SS --> SDB
s1 --> RES
RES --> SS
classDef stub stroke-dasharray:4
class s1 stub
Medium density¶
Under medium-density, the plan selects T5.
It climbs the same steps, T3, T2 and T5, but the larger target leaves more of the diagram as the author drew it.
medium-density targets 15 nodes and a score of 30.
Overview source
flowchart TB
g3[["Customer → Web firewall (+1)"]]
g1[["Admin console (+4)"]]
g4[["Refund service (+5)"]]
g5[["Orders (+3)"]]
LOG["Audit log"]
DB[("Orders DB")]
g2[["Shipping service (+5)"]]
RES["Reservation service"]
g6[["Stock service → Stock DB (+1)"]]
g3 --> g1
g1 --> g4
g1 --> g5
g4 --> LOG
g5 --> g4
g5 --> LOG
g5 --> DB
g5 --> g2
g5 --> RES
RES --> g6
The overview as drawn
flowchart TB
g3[["Customer → Web firewall (+1)"]]
g1[["Admin console (+4)"]]
g4[["Refund service (+5)"]]
g5[["Orders (+3)"]]
LOG["Audit log"]
DB[("Orders DB")]
g2[["Shipping service (+5)"]]
RES["Reservation service"]
g6[["Stock service → Stock DB (+1)"]]
g3 --> g1
g1 --> g4
g1 --> g5
g4 --> LOG
g5 --> g4
g5 --> LOG
g5 --> DB
g5 --> g2
g5 --> RES
RES --> g6
What the parser returned for the overview: 9 nodes, 10 edges, 0 subgraphs
target: 15 nodes, score 30
tier: T5
overview: 9 nodes, score 14
groups: Admin console (+4) [T5, leaf fold]; Shipping service (+5) [T5, leaf fold]; Customer → Web firewall (+1) [T5, leaf fold]; Refund service (+5) [T5, leaf fold]; Orders (+3) [T3, author region]; Stock service → Stock DB (+1) [T5, leaf fold]
declared loss: 0 new reachable pairs (after each tier: T1 0, T3 0, T2 0, T4 0, T5 0)
rows dropped: 0
ladder: T1 29 nodes, score 62.7; then T3 21 nodes, score 32.5; then T2 18 nodes, score 28; then T5 9 nodes, score 14
Detail of Admin console (+4): 8 nodes, score 15.95
flowchart TB
ADMIN["Administrator"]
s3(["Refund service"])
CACHE[("Session cache")]
s2(["Order service (in Orders (+3))"])
s1(["Customer → Web firewall (+1)"])
subgraph web["Web"]
ADM["Admin console"]
API["API gateway"]
UI["Storefront"]
end
ADMIN --> ADM
ADM --> API
ADM --> s3
API --> CACHE
API --> s2
s1 --> UI
UI --> API
classDef stub stroke-dasharray:4
class s1,s2,s3 stub
Detail of Shipping service (+5): 7 nodes, score 14.85
flowchart TB
s1(["Orders (+3)"])
CAR["Carrier"]
PUSH["Push alert"]
SMS["Text message"]
subgraph shipping["Shipping"]
SHS["Shipping service"]
LBL["Label printer"]
TRK["Tracking feed"]
end
s1 --> SHS
SHS --> CAR
SHS --> LBL
SHS --> PUSH
SHS --> SMS
CAR --> TRK
TRK --> SHS
classDef stub stroke-dasharray:4
class s1 stub
Detail of Customer → Web firewall (+1): 4 nodes, score 5.5
flowchart TB
CUST["Customer"]
CDN["Content cache"]
WAF["Web firewall"]
s1(["Storefront (in Admin console (+4))"])
CUST --> CDN
CDN --> WAF
WAF --> s1
classDef stub stroke-dasharray:4
class s1 stub
Detail of Refund service (+5): 9 nodes, score 18.15
flowchart TB
s1(["Admin console (+4)"])
s3(["Audit log"])
PDB[("Payments DB")]
PSP["Card processor"]
MAIL["Email"]
s2(["Orders (+3)"])
subgraph billing["Billing"]
REF["Refund service"]
PS["Payment service"]
INV["Invoice service"]
end
s1 --> REF
REF --> PS
REF --> s3
PS --> s3
PS --> INV
PS --> PDB
PS --> PSP
INV --> MAIL
s2 --> PS
classDef stub stroke-dasharray:4
class s1,s2,s3 stub
Detail of Orders (+3): 9 nodes, score 17.6
flowchart TB
s1(["Admin console (+4)"])
s2(["Orders DB"])
s3(["Audit log"])
s4(["Payment service (in Refund service (+5))"])
s5(["Reservation service"])
s6(["Shipping service"])
subgraph orders["Orders"]
OS["Order service"]
OQ["Order queue"]
OW["Order worker"]
end
s1 --> OS
OS --> OQ
OS --> s2
OS --> s3
OQ --> OW
OW --> s4
OW --> s5
OW --> s6
classDef stub stroke-dasharray:4
class s1,s2,s3,s4,s5,s6 stub
Detail of Stock service → Stock DB (+1): 4 nodes, score 5.5
flowchart TB
WH["Warehouse feed"]
SS["Stock service"]
SDB[("Stock DB")]
s1(["Reservation service"])
WH --> SS
SS --> SDB
s1 --> SS
classDef stub stroke-dasharray:4
class s1 stub
High density¶
Under high-density, the plan selects T3.
Folding author regions in T3 is enough to reach this target.
high-density targets 25 nodes and a score of 40.
Overview source
flowchart TB
ADMIN["Administrator"]
g1[["Web (+3)"]]
CACHE[("Session cache")]
g2[["Billing (+3)"]]
g4[["Orders (+3)"]]
LOG["Audit log"]
MAIL["Email"]
PDB[("Payments DB")]
PSP["Card processor"]
DB[("Orders DB")]
g3[["Shipping (+3)"]]
RES["Reservation service"]
CAR["Carrier"]
PUSH["Push alert"]
SMS["Text message"]
SS["Stock service"]
SDB[("Stock DB")]
CUST["Customer"]
CDN["Content cache"]
WAF["Web firewall"]
WH["Warehouse feed"]
ADMIN --> g1
g1 --> CACHE
g1 --> g2
g1 --> g4
g2 --> LOG
g2 --> MAIL
g2 --> PDB
g2 --> PSP
g4 --> g2
g4 --> LOG
g4 --> DB
g4 --> g3
g4 --> RES
g3 --> CAR
g3 --> PUSH
g3 --> SMS
RES --> SS
CAR --> g3
SS --> SDB
CUST --> CDN
CDN --> WAF
WAF --> g1
WH --> SS
The overview as drawn
flowchart TB
ADMIN["Administrator"]
g1[["Web (+3)"]]
CACHE[("Session cache")]
g2[["Billing (+3)"]]
g4[["Orders (+3)"]]
LOG["Audit log"]
MAIL["Email"]
PDB[("Payments DB")]
PSP["Card processor"]
DB[("Orders DB")]
g3[["Shipping (+3)"]]
RES["Reservation service"]
CAR["Carrier"]
PUSH["Push alert"]
SMS["Text message"]
SS["Stock service"]
SDB[("Stock DB")]
CUST["Customer"]
CDN["Content cache"]
WAF["Web firewall"]
WH["Warehouse feed"]
ADMIN --> g1
g1 --> CACHE
g1 --> g2
g1 --> g4
g2 --> LOG
g2 --> MAIL
g2 --> PDB
g2 --> PSP
g4 --> g2
g4 --> LOG
g4 --> DB
g4 --> g3
g4 --> RES
g3 --> CAR
g3 --> PUSH
g3 --> SMS
RES --> SS
CAR --> g3
SS --> SDB
CUST --> CDN
CDN --> WAF
WAF --> g1
WH --> SS
What the parser returned for the overview: 21 nodes, 23 edges, 0 subgraphs
target: 25 nodes, score 40
tier: T3
overview: 21 nodes, score 32.5
groups: Web (+3) [T3, author region]; Billing (+3) [T3, author region]; Shipping (+3) [T3, author region]; Orders (+3) [T3, author region]
declared loss: 0 new reachable pairs (after each tier: T1 0, T3 0)
rows dropped: 0
ladder: T1 29 nodes, score 62.7; then T3 21 nodes, score 32.5
Detail of Web (+3): 8 nodes, score 15.95
flowchart TB
s1(["Administrator"])
s4(["Refund service (in Billing (+3))"])
s2(["Session cache"])
s3(["Order service (in Orders (+3))"])
s5(["Web firewall"])
subgraph web["Web"]
ADM["Admin console"]
API["API gateway"]
UI["Storefront"]
end
s1 --> ADM
ADM --> API
ADM --> s4
API --> s2
API --> s3
s5 --> UI
UI --> API
classDef stub stroke-dasharray:4
class s1,s2,s3,s4,s5 stub
Detail of Billing (+3): 9 nodes, score 18.15
flowchart TB
s1(["Web (+3)"])
s3(["Audit log"])
s5(["Payments DB"])
s6(["Card processor"])
s4(["Email"])
s2(["Orders (+3)"])
subgraph billing["Billing"]
REF["Refund service"]
PS["Payment service"]
INV["Invoice service"]
end
s1 --> REF
REF --> PS
REF --> s3
PS --> s3
PS --> INV
PS --> s5
PS --> s6
INV --> s4
s2 --> PS
classDef stub stroke-dasharray:4
class s1,s2,s3,s4,s5,s6 stub
Detail of Shipping (+3): 7 nodes, score 14.85
flowchart TB
s1(["Orders (+3)"])
s2(["Carrier"])
s3(["Push alert"])
s4(["Text message"])
subgraph shipping["Shipping"]
SHS["Shipping service"]
LBL["Label printer"]
TRK["Tracking feed"]
end
s1 --> SHS
SHS --> LBL
SHS --> s2
SHS --> s3
SHS --> s4
s2 --> TRK
TRK --> SHS
classDef stub stroke-dasharray:4
class s1,s2,s3,s4 stub
Detail of Orders (+3): 9 nodes, score 17.6
flowchart TB
s1(["Web (+3)"])
s2(["Orders DB"])
s3(["Audit log"])
s4(["Payment service (in Billing (+3))"])
s5(["Reservation service"])
s6(["Shipping service (in Shipping (+3))"])
subgraph orders["Orders"]
OS["Order service"]
OQ["Order queue"]
OW["Order worker"]
end
s1 --> OS
OS --> OQ
OS --> s2
OS --> s3
OQ --> OW
OW --> s4
OW --> s5
OW --> s6
classDef stub stroke-dasharray:4
class s1,s2,s3,s4,s5,s6 stub
A decision-heavy example¶
The second diagram is a loan underwriting flow with no author subgraphs, where most steps are decisions. Every decision is protected, so before decision regions no tier could fold it under any preset. It scores 65.
Mermaid source
flowchart TD
start(["Application received"]) --> kyc{"Identity verified?"}
kyc -->|No| kycdoc["Request ID documents"] --> kycwait{"Documents within 14 days?"}
kycwait -->|No| declinekyc(["Decline: identity"])
kycwait -->|Yes| kyc
kyc -->|Yes| sanctions{"Sanctions list hit?"}
sanctions -->|Yes| compliance["Refer to compliance"] --> compok{"Compliance clears?"}
compok -->|No| declinesan(["Decline: sanctions"])
compok -->|Yes| bureau
sanctions -->|No| bureau["Pull credit bureau file"]
bureau --> thin{"Thin file?"}
thin -->|Yes| alt["Score with open banking data"] --> altscore{"Alt score above 620?"}
altscore -->|No| declinethin(["Decline: insufficient history"])
altscore -->|Yes| income
thin -->|No| score{"Bureau score above 680?"}
score -->|No| score2{"Score above 600?"}
score2 -->|No| declinescore(["Decline: credit score"])
score2 -->|Yes| secured{"Offer secured product?"}
secured -->|Yes| collat["Collateral valuation"] --> ltv{"LTV under 80%?"}
ltv -->|No| declineltv(["Decline: collateral"])
ltv -->|Yes| income
secured -->|No| declinescore
score -->|Yes| income["Verify income"]
income --> dti{"Debt-to-income under 40%?"}
dti -->|No| reduce{"Lower amount fits DTI?"}
reduce -->|No| declinedti(["Decline: affordability"])
reduce -->|Yes| counter["Counter-offer lower amount"] --> accept{"Applicant accepts?"}
accept -->|No| withdrawn(["Withdrawn"])
accept -->|Yes| fraud
dti -->|Yes| fraud{"Fraud model flags?"}
fraud -->|Yes| manual["Manual fraud review"] --> fraudok{"Reviewer clears?"}
fraudok -->|No| declinefraud(["Decline: fraud"])
fraudok -->|Yes| pricing
fraud -->|No| pricing["Risk-based pricing"]
pricing --> large{"Amount above 50k?"}
large -->|Yes| senior["Senior underwriter sign-off"] --> signed{"Signed off?"}
signed -->|No| declinesenior(["Decline: underwriter"])
signed -->|Yes| offer
large -->|No| offer["Issue offer"]
offer --> esign{"E-signed within 30 days?"}
esign -->|No| expired(["Offer expired"])
esign -->|Yes| disburse(["Disburse funds"])
The diagram as drawn
flowchart TD
start(["Application received"]) --> kyc{"Identity verified?"}
kyc -->|No| kycdoc["Request ID documents"] --> kycwait{"Documents within 14 days?"}
kycwait -->|No| declinekyc(["Decline: identity"])
kycwait -->|Yes| kyc
kyc -->|Yes| sanctions{"Sanctions list hit?"}
sanctions -->|Yes| compliance["Refer to compliance"] --> compok{"Compliance clears?"}
compok -->|No| declinesan(["Decline: sanctions"])
compok -->|Yes| bureau
sanctions -->|No| bureau["Pull credit bureau file"]
bureau --> thin{"Thin file?"}
thin -->|Yes| alt["Score with open banking data"] --> altscore{"Alt score above 620?"}
altscore -->|No| declinethin(["Decline: insufficient history"])
altscore -->|Yes| income
thin -->|No| score{"Bureau score above 680?"}
score -->|No| score2{"Score above 600?"}
score2 -->|No| declinescore(["Decline: credit score"])
score2 -->|Yes| secured{"Offer secured product?"}
secured -->|Yes| collat["Collateral valuation"] --> ltv{"LTV under 80%?"}
ltv -->|No| declineltv(["Decline: collateral"])
ltv -->|Yes| income
secured -->|No| declinescore
score -->|Yes| income["Verify income"]
income --> dti{"Debt-to-income under 40%?"}
dti -->|No| reduce{"Lower amount fits DTI?"}
reduce -->|No| declinedti(["Decline: affordability"])
reduce -->|Yes| counter["Counter-offer lower amount"] --> accept{"Applicant accepts?"}
accept -->|No| withdrawn(["Withdrawn"])
accept -->|Yes| fraud
dti -->|Yes| fraud{"Fraud model flags?"}
fraud -->|Yes| manual["Manual fraud review"] --> fraudok{"Reviewer clears?"}
fraudok -->|No| declinefraud(["Decline: fraud"])
fraudok -->|Yes| pricing
fraud -->|No| pricing["Risk-based pricing"]
pricing --> large{"Amount above 50k?"}
large -->|Yes| senior["Senior underwriter sign-off"] --> signed{"Signed off?"}
signed -->|No| declinesenior(["Decline: underwriter"])
signed -->|Yes| offer
large -->|No| offer["Issue offer"]
offer --> esign{"E-signed within 30 days?"}
esign -->|No| expired(["Offer expired"])
esign -->|Yes| disburse(["Disburse funds"])
What the parser returned: 41 nodes, 48 edges, 0 subgraphs
depth = max(subgraph nesting 0, tree depth 0) = 0
score = (nodes + 0.5 × edges + 3 × subgraphs) × (1 + 0.1 × depth)
= (41 + 0.5 × 48 + 3 × 0) × (1 + 0.1 × 0)
= (41 + 24 + 0) × 1
= 65 × 1
= 65
Each preset's plan¶
Each group named after a question is a decision region: the decision with its branches and outcomes, up to where they rejoin.
| Preset | nodeTarget / vcsTarget |
Tier | Overview nodes / score | Details | New reachable pairs |
|---|---|---|---|---|---|
low-density |
10 / 20 | T5 | 10 / 15 | 7 | 0 |
medium-density |
15 / 30 | T2 | 10 / 14.5 | 7 | 0 |
high-density |
25 / 40 | T2 | 16 / 24 | 5 | 0 |
Low density, decisions¶
Under low-density, the plan selects T5.
Decision regions and chain stages through them, in T2, leave the overview just over its target.
So in T5 a leaf folds into the node it hangs from.
low-density targets 10 nodes and a score of 20.
Overview source
flowchart TB
start(["Application received"])
g6[["Identity verified? (+3)"]]
g3[["Sanctions list hit? (+4)"]]
thin{"Thin file?"}
g2[["Score with open banking data (+2)"]]
g4[["Bureau score above 680? (+6)"]]
income["Verify income"]
g1[["Debt-to-income under 40%? (+5)"]]
g5[["Fraud model flags? → Risk-based pricing (+3)"]]
g7[["Amount above 50k? (+7)"]]
start --> g6
g6 -->|"Yes"| g3
g3 --> thin
thin -->|"Yes"| g2
thin -->|"No"| g4
g2 -->|"Yes"| income
g4 -->|"Yes"| income
income --> g1
g1 -->|"Yes"| g5
g5 --> g7
The overview as drawn
flowchart TB
start(["Application received"])
g6[["Identity verified? (+3)"]]
g3[["Sanctions list hit? (+4)"]]
thin{"Thin file?"}
g2[["Score with open banking data (+2)"]]
g4[["Bureau score above 680? (+6)"]]
income["Verify income"]
g1[["Debt-to-income under 40%? (+5)"]]
g5[["Fraud model flags? → Risk-based pricing (+3)"]]
g7[["Amount above 50k? (+7)"]]
start --> g6
g6 -->|"Yes"| g3
g3 --> thin
thin -->|"Yes"| g2
thin -->|"No"| g4
g2 -->|"Yes"| income
g4 -->|"Yes"| income
income --> g1
g1 -->|"Yes"| g5
g5 --> g7
What the parser returned for the overview: 10 nodes, 10 edges, 0 subgraphs
target: 10 nodes, score 20
tier: T5
overview: 10 nodes, score 15
groups: Debt-to-income under 40%? (+5) [T2, decision region]; Score with open banking data (+2) [T2, chain stage]; Sanctions list hit? (+4) [T2, chain stage]; Bureau score above 680? (+6) [T2, decision region]; Fraud model flags? → Risk-based pricing (+3) [T2, chain stage]; Identity verified? (+3) [T2, decision region]; Amount above 50k? (+7) [T5, leaf fold]
declared loss: 0 new reachable pairs (after each tier: T1 0, T2 0, T3 0, T4 0, T5 0)
rows dropped: 0
ladder: T1 41 nodes, score 65; then T2 11 nodes, score 16.5; then T5 10 nodes, score 15
Detail of Debt-to-income under 40%? (+5): 8 nodes, score 12
flowchart TB
s2(["Verify income"])
dti{"Debt-to-income under 40%?"}
reduce{"Lower amount fits DTI?"}
s1(["Fraud model flags?"])
counter["Counter-offer lower amount"]
declinedti(["Decline: affordability"])
accept{"Applicant accepts?"}
withdrawn(["Withdrawn"])
s2 --> dti
dti -->|"No"| reduce
dti -->|"Yes"| s1
reduce -->|"Yes"| counter
reduce -->|"No"| declinedti
counter --> accept
accept -->|"Yes"| s1
accept -->|"No"| withdrawn
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Score with open banking data (+2): 5 nodes, score 7
flowchart TB
s2(["Thin file?"])
alt["Score with open banking data"]
altscore{"Alt score above 620?"}
declinethin(["Decline: insufficient history"])
s1(["Verify income"])
s2 -->|"Yes"| alt
alt --> altscore
altscore -->|"No"| declinethin
altscore -->|"Yes"| s1
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Sanctions list hit? (+4): 7 nodes, score 10.5
flowchart TB
s1(["Identity verified? (+3)"])
sanctions{"Sanctions list hit?"}
bureau["Pull credit bureau file"]
compliance["Refer to compliance"]
s2(["Thin file?"])
compok{"Compliance clears?"}
declinesan(["Decline: sanctions"])
s1 -->|"Yes"| sanctions
sanctions -->|"No"| bureau
sanctions -->|"Yes"| compliance
bureau --> s2
compliance --> compok
compok -->|"Yes"| bureau
compok -->|"No"| declinesan
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Bureau score above 680? (+6): 9 nodes, score 14
flowchart TB
s2(["Thin file?"])
score{"Bureau score above 680?"}
s1(["Verify income"])
score2{"Score above 600?"}
declinescore(["Decline: credit score"])
secured{"Offer secured product?"}
collat["Collateral valuation"]
ltv{"LTV under 80%?"}
declineltv(["Decline: collateral"])
s2 -->|"No"| score
score -->|"Yes"| s1
score -->|"No"| score2
score2 -->|"No"| declinescore
score2 -->|"Yes"| secured
secured -->|"No"| declinescore
secured -->|"Yes"| collat
collat --> ltv
ltv -->|"Yes"| s1
ltv -->|"No"| declineltv
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Fraud model flags? → Risk-based pricing (+3): 7 nodes, score 10.5
flowchart TB
s1(["Debt-to-income under 40%? (+5)"])
fraud{"Fraud model flags?"}
manual["Manual fraud review"]
pricing["Risk-based pricing"]
fraudok{"Reviewer clears?"}
s2(["Amount above 50k?"])
declinefraud(["Decline: fraud"])
s1 -->|"Yes"| fraud
fraud -->|"Yes"| manual
fraud -->|"No"| pricing
manual --> fraudok
pricing --> s2
fraudok -->|"Yes"| pricing
fraudok -->|"No"| declinefraud
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Identity verified? (+3): 6 nodes, score 9
flowchart TB
s2(["Application received"])
kyc{"Identity verified?"}
kycdoc["Request ID documents"]
s1(["Sanctions list hit?"])
kycwait{"Documents within 14 days?"}
declinekyc(["Decline: identity"])
s2 --> kyc
kyc -->|"No"| kycdoc
kyc -->|"Yes"| s1
kycdoc --> kycwait
kycwait -->|"Yes"| kyc
kycwait -->|"No"| declinekyc
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Amount above 50k? (+7): 9 nodes, score 13.5
flowchart TB
s1(["Fraud model flags? → Risk-based pricing (+3)"])
large{"Amount above 50k?"}
offer["Issue offer"]
senior["Senior underwriter sign-off"]
esign{"E-signed within 30 days?"}
signed{"Signed off?"}
disburse(["Disburse funds"])
expired(["Offer expired"])
declinesenior(["Decline: underwriter"])
s1 --> large
large -->|"No"| offer
large -->|"Yes"| senior
offer --> esign
senior --> signed
esign -->|"Yes"| disburse
esign -->|"No"| expired
signed -->|"Yes"| offer
signed -->|"No"| declinesenior
classDef stub stroke-dasharray:4
class s1 stub
Medium density, decisions¶
Under medium-density, the plan selects T2.
Decision regions in T2 alone reach the target, and the remaining ones fold evenly.
medium-density targets 15 nodes and a score of 30.
Overview source
flowchart TB
start(["Application received"])
g5[["Identity verified? (+3)"]]
g3[["Sanctions list hit? (+4)"]]
g2[["Thin file? (+10)"]]
income["Verify income"]
g1[["Debt-to-income under 40%? (+5)"]]
g4[["Fraud model flags? → Risk-based pricing (+3)"]]
g6[["Amount above 50k? (+3)"]]
offer["Issue offer"]
g7[["E-signed within 30 days? (+2)"]]
start --> g5
g5 -->|"Yes"| g3
g3 --> g2
g2 -->|"Yes"| income
income --> g1
g1 -->|"Yes"| g4
g4 --> g6
g6 --> offer
offer --> g7
The overview as drawn
flowchart TB
start(["Application received"])
g5[["Identity verified? (+3)"]]
g3[["Sanctions list hit? (+4)"]]
g2[["Thin file? (+10)"]]
income["Verify income"]
g1[["Debt-to-income under 40%? (+5)"]]
g4[["Fraud model flags? → Risk-based pricing (+3)"]]
g6[["Amount above 50k? (+3)"]]
offer["Issue offer"]
g7[["E-signed within 30 days? (+2)"]]
start --> g5
g5 -->|"Yes"| g3
g3 --> g2
g2 -->|"Yes"| income
income --> g1
g1 -->|"Yes"| g4
g4 --> g6
g6 --> offer
offer --> g7
What the parser returned for the overview: 10 nodes, 9 edges, 0 subgraphs
target: 15 nodes, score 30
tier: T2
overview: 10 nodes, score 14.5
groups: Debt-to-income under 40%? (+5) [T2, decision region]; Thin file? (+10) [T2, decision region]; Sanctions list hit? (+4) [T2, chain stage]; Fraud model flags? → Risk-based pricing (+3) [T2, chain stage]; Identity verified? (+3) [T2, decision region]; Amount above 50k? (+3) [T2, decision region]; E-signed within 30 days? (+2) [T2, decision region]
declared loss: 0 new reachable pairs (after each tier: T1 0, T2 0)
rows dropped: 0
ladder: T1 41 nodes, score 65; then T2 10 nodes, score 14.5
Detail of Debt-to-income under 40%? (+5): 8 nodes, score 12
flowchart TB
s2(["Verify income"])
dti{"Debt-to-income under 40%?"}
reduce{"Lower amount fits DTI?"}
s1(["Fraud model flags?"])
counter["Counter-offer lower amount"]
declinedti(["Decline: affordability"])
accept{"Applicant accepts?"}
withdrawn(["Withdrawn"])
s2 --> dti
dti -->|"No"| reduce
dti -->|"Yes"| s1
reduce -->|"Yes"| counter
reduce -->|"No"| declinedti
counter --> accept
accept -->|"Yes"| s1
accept -->|"No"| withdrawn
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Thin file? (+10): 13 nodes, score 20.5
flowchart TB
s1(["Sanctions list hit? (+4)"])
thin{"Thin file?"}
alt["Score with open banking data"]
score{"Bureau score above 680?"}
altscore{"Alt score above 620?"}
s2(["Verify income"])
score2{"Score above 600?"}
declinethin(["Decline: insufficient history"])
declinescore(["Decline: credit score"])
secured{"Offer secured product?"}
collat["Collateral valuation"]
ltv{"LTV under 80%?"}
declineltv(["Decline: collateral"])
s1 --> thin
thin -->|"Yes"| alt
thin -->|"No"| score
alt --> altscore
score -->|"Yes"| s2
score -->|"No"| score2
altscore -->|"Yes"| s2
altscore -->|"No"| declinethin
score2 -->|"No"| declinescore
score2 -->|"Yes"| secured
secured -->|"No"| declinescore
secured -->|"Yes"| collat
collat --> ltv
ltv -->|"Yes"| s2
ltv -->|"No"| declineltv
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Sanctions list hit? (+4): 7 nodes, score 10.5
flowchart TB
s1(["Identity verified? (+3)"])
sanctions{"Sanctions list hit?"}
bureau["Pull credit bureau file"]
compliance["Refer to compliance"]
s2(["Thin file?"])
compok{"Compliance clears?"}
declinesan(["Decline: sanctions"])
s1 -->|"Yes"| sanctions
sanctions -->|"No"| bureau
sanctions -->|"Yes"| compliance
bureau --> s2
compliance --> compok
compok -->|"Yes"| bureau
compok -->|"No"| declinesan
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Fraud model flags? → Risk-based pricing (+3): 7 nodes, score 10.5
flowchart TB
s1(["Debt-to-income under 40%? (+5)"])
fraud{"Fraud model flags?"}
manual["Manual fraud review"]
pricing["Risk-based pricing"]
fraudok{"Reviewer clears?"}
s2(["Amount above 50k?"])
declinefraud(["Decline: fraud"])
s1 -->|"Yes"| fraud
fraud -->|"Yes"| manual
fraud -->|"No"| pricing
manual --> fraudok
pricing --> s2
fraudok -->|"Yes"| pricing
fraudok -->|"No"| declinefraud
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Identity verified? (+3): 6 nodes, score 9
flowchart TB
s2(["Application received"])
kyc{"Identity verified?"}
kycdoc["Request ID documents"]
s1(["Sanctions list hit?"])
kycwait{"Documents within 14 days?"}
declinekyc(["Decline: identity"])
s2 --> kyc
kyc -->|"No"| kycdoc
kyc -->|"Yes"| s1
kycdoc --> kycwait
kycwait -->|"Yes"| kyc
kycwait -->|"No"| declinekyc
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Amount above 50k? (+3): 6 nodes, score 9
flowchart TB
s1(["Fraud model flags? → Risk-based pricing (+3)"])
large{"Amount above 50k?"}
s2(["Issue offer"])
senior["Senior underwriter sign-off"]
signed{"Signed off?"}
declinesenior(["Decline: underwriter"])
s1 --> large
large -->|"No"| s2
large -->|"Yes"| senior
senior --> signed
signed -->|"Yes"| s2
signed -->|"No"| declinesenior
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of E-signed within 30 days? (+2): 4 nodes, score 5.5
flowchart TB
s1(["Issue offer"])
esign{"E-signed within 30 days?"}
disburse(["Disburse funds"])
expired(["Offer expired"])
s1 --> esign
esign -->|"Yes"| disburse
esign -->|"No"| expired
classDef stub stroke-dasharray:4
class s1 stub
High density, decisions¶
Under high-density, the plan selects T2.
Fewer regions fold in T2, so more of the decisions stay in view.
high-density targets 25 nodes and a score of 40.
Overview source
flowchart TB
start(["Application received"])
g5[["Identity verified? (+3)"]]
g3[["Sanctions list hit? (+3)"]]
bureau["Pull credit bureau file"]
g2[["Thin file? (+10)"]]
income["Verify income"]
g1[["Debt-to-income under 40%? (+5)"]]
g4[["Fraud model flags? → Risk-based pricing (+3)"]]
large{"Amount above 50k?"}
offer["Issue offer"]
senior["Senior underwriter sign-off"]
esign{"E-signed within 30 days?"}
signed{"Signed off?"}
disburse(["Disburse funds"])
expired(["Offer expired"])
declinesenior(["Decline: underwriter"])
start --> g5
g5 -->|"Yes"| g3
g3 --> bureau
bureau --> g2
g2 -->|"Yes"| income
income --> g1
g1 -->|"Yes"| g4
g4 --> large
large -->|"No"| offer
large -->|"Yes"| senior
offer --> esign
senior --> signed
esign -->|"Yes"| disburse
esign -->|"No"| expired
signed -->|"Yes"| offer
signed -->|"No"| declinesenior
The overview as drawn
flowchart TB
start(["Application received"])
g5[["Identity verified? (+3)"]]
g3[["Sanctions list hit? (+3)"]]
bureau["Pull credit bureau file"]
g2[["Thin file? (+10)"]]
income["Verify income"]
g1[["Debt-to-income under 40%? (+5)"]]
g4[["Fraud model flags? → Risk-based pricing (+3)"]]
large{"Amount above 50k?"}
offer["Issue offer"]
senior["Senior underwriter sign-off"]
esign{"E-signed within 30 days?"}
signed{"Signed off?"}
disburse(["Disburse funds"])
expired(["Offer expired"])
declinesenior(["Decline: underwriter"])
start --> g5
g5 -->|"Yes"| g3
g3 --> bureau
bureau --> g2
g2 -->|"Yes"| income
income --> g1
g1 -->|"Yes"| g4
g4 --> large
large -->|"No"| offer
large -->|"Yes"| senior
offer --> esign
senior --> signed
esign -->|"Yes"| disburse
esign -->|"No"| expired
signed -->|"Yes"| offer
signed -->|"No"| declinesenior
What the parser returned for the overview: 16 nodes, 16 edges, 0 subgraphs
target: 25 nodes, score 40
tier: T2
overview: 16 nodes, score 24
groups: Debt-to-income under 40%? (+5) [T2, decision region]; Thin file? (+10) [T2, decision region]; Sanctions list hit? (+3) [T2, decision region]; Fraud model flags? → Risk-based pricing (+3) [T2, chain stage]; Identity verified? (+3) [T2, decision region]
declared loss: 0 new reachable pairs (after each tier: T1 0, T2 0)
rows dropped: 0
ladder: T1 41 nodes, score 65; then T2 16 nodes, score 24
Detail of Debt-to-income under 40%? (+5): 8 nodes, score 12
flowchart TB
s2(["Verify income"])
dti{"Debt-to-income under 40%?"}
reduce{"Lower amount fits DTI?"}
s1(["Fraud model flags?"])
counter["Counter-offer lower amount"]
declinedti(["Decline: affordability"])
accept{"Applicant accepts?"}
withdrawn(["Withdrawn"])
s2 --> dti
dti -->|"No"| reduce
dti -->|"Yes"| s1
reduce -->|"Yes"| counter
reduce -->|"No"| declinedti
counter --> accept
accept -->|"Yes"| s1
accept -->|"No"| withdrawn
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Thin file? (+10): 13 nodes, score 20.5
flowchart TB
s1(["Pull credit bureau file"])
thin{"Thin file?"}
alt["Score with open banking data"]
score{"Bureau score above 680?"}
altscore{"Alt score above 620?"}
s2(["Verify income"])
score2{"Score above 600?"}
declinethin(["Decline: insufficient history"])
declinescore(["Decline: credit score"])
secured{"Offer secured product?"}
collat["Collateral valuation"]
ltv{"LTV under 80%?"}
declineltv(["Decline: collateral"])
s1 --> thin
thin -->|"Yes"| alt
thin -->|"No"| score
alt --> altscore
score -->|"Yes"| s2
score -->|"No"| score2
altscore -->|"Yes"| s2
altscore -->|"No"| declinethin
score2 -->|"No"| declinescore
score2 -->|"Yes"| secured
secured -->|"No"| declinescore
secured -->|"Yes"| collat
collat --> ltv
ltv -->|"Yes"| s2
ltv -->|"No"| declineltv
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Sanctions list hit? (+3): 6 nodes, score 9
flowchart TB
s1(["Identity verified? (+3)"])
sanctions{"Sanctions list hit?"}
compliance["Refer to compliance"]
s2(["Pull credit bureau file"])
compok{"Compliance clears?"}
declinesan(["Decline: sanctions"])
s1 -->|"Yes"| sanctions
sanctions -->|"Yes"| compliance
sanctions -->|"No"| s2
compliance --> compok
compok -->|"Yes"| s2
compok -->|"No"| declinesan
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Fraud model flags? → Risk-based pricing (+3): 7 nodes, score 10.5
flowchart TB
s1(["Debt-to-income under 40%? (+5)"])
fraud{"Fraud model flags?"}
manual["Manual fraud review"]
pricing["Risk-based pricing"]
fraudok{"Reviewer clears?"}
s2(["Amount above 50k?"])
declinefraud(["Decline: fraud"])
s1 -->|"Yes"| fraud
fraud -->|"Yes"| manual
fraud -->|"No"| pricing
manual --> fraudok
pricing --> s2
fraudok -->|"Yes"| pricing
fraudok -->|"No"| declinefraud
classDef stub stroke-dasharray:4
class s1,s2 stub
Detail of Identity verified? (+3): 6 nodes, score 9
flowchart TB
s2(["Application received"])
kyc{"Identity verified?"}
kycdoc["Request ID documents"]
s1(["Sanctions list hit?"])
kycwait{"Documents within 14 days?"}
declinekyc(["Decline: identity"])
s2 --> kyc
kyc -->|"No"| kycdoc
kyc -->|"Yes"| s1
kycdoc --> kycwait
kycwait -->|"Yes"| kyc
kycwait -->|"No"| declinekyc
classDef stub stroke-dasharray:4
class s1,s2 stub