Skip to content

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 erDiagram never merges two relationship labels, so there its leaves stay open.
  • Shared terminals stay in view. A shared terminal is a flowchart or stateDiagram node 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. A Notify on-call step every failure ends at is one, and so is a final Refunded state. So is a Cancelled state that leads only to Refunded and Voided.

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 T3 onwards.
  • 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