Grouping & membership
boundary groups nodes into frames for one view; facet marks which declared sets an element belongs to.
Semantic clusters (boundary)
Section titled “Semantic clusters (boundary)”Drawn with Group by: Boundary. Turn on the same toggle in the app to get this diagram. Read the spec
// Semantic clusters via `boundary` / `contains` (Issue #1822, P2b).//// A `boundary` block declares a named cluster of system-view nodes, drawn as the// second "Group by" axis of the system view (the first is team ownership —// see team-ownership.krs). With `groupBy: "boundary"` the renderer bands each// boundary's members as a dependency-ordered group and frames it, exactly like// the team axis — but grouped by the author's *semantic* clusters instead of by// org chart. The two axes are independent and mutually exclusive.//// Here the "Payments" and "Catalog" boundaries cut across what could be a// different team structure: a boundary groups by meaning ("everything payments"),// not by who owns it. Un-clustered nodes (the shared Order DB) fall into a// trailing band below the boundary frames.//// Members can live on drill-down levels too: grouping resolves per view,// against the nodes rendered at the level being drawn (Issue #1983). The// Payment domain nested inside Checkout is framed on Checkout's drill-down// view, and its Authorize usecase on Payment's domain view — one boundary,// disjoint frames on several levels. Keep "Group by: Boundary" on while you// drill to see each level's frame.//// `boundary` is core notation from `.krs language v2.0` (docs/spec/syntax.md// § Grouping the system view).
system Marketplace { label "Marketplace"
service Checkout { label "Checkout" domain Cart { label "Cart" } domain Payment { label "Payment" usecase Authorize { label "Authorize card" } } } service Billing { label "Billing" } service Wallet { label "Wallet" } service Search { label "Search" } service Inventory { label "Inventory" } service Gateway { label "API Gateway" }
database OrderDB { label "Order DB" }
Gateway -> Search "route" Gateway -> Checkout "route" Checkout -> Billing "charge" Billing -> Wallet "settle" Checkout -> Inventory "reserve" Search -> Inventory "read" Checkout -> OrderDB "persist"}
boundary payments { label "Payments" contains Checkout contains Billing contains Wallet contains Payment contains Authorize}
boundary catalog { label "Catalog" contains Search contains Inventory}One node in two boundaries
Section titled “One node in two boundaries”Drawn with Group by: Boundary. The two frames overlap over the shared node; the node itself is drawn once. Read the spec
// A node in more than one `boundary` (Issue #2161).//// Ledger is listed in BOTH `payments` and `pci` below. Both memberships are// kept in the model, and with "Group by: Boundary" on you can see both: the// `pci` frame reaches up out of its own band to wrap Ledger where it sits at// the bottom of `payments`, so the two frames OVERLAP over that one card.//// Ledger is still drawn exactly ONCE. What overlaps is the frames, not the// node — a banded layout gives every node one row, and multi-membership is// expressed by letting a frame leave its band, never by copying the card.//// Each boundary has its own colour, cycled by declaration order, and fills it// faintly. That is not decoration: in one shared colour, two dashed outlines// around the same card read as one frame NESTED in the other. The colours (and// the third tone where the two fills composite) are what make it read as a// crossing instead. The frame titles take the same colour, so you can tell// which outline belongs to which boundary.//// Two things decide whether you see the overlap at all://// 1. Boundaries that share a member are banded next to each other where the// dependency flow allows it, and the shared node is seated on the row that// TOUCHES the other band. Here Ledger sits at the bottom of `payments`,// against `pci`. Without that seam there is nothing to reach across.// 2. A frame is widened only when the corridor to the card holds no card that// is NOT its member. A frame must never enclose a non-member, and that// rule wins over showing the containment.//// To watch rule 2 refuse — and see what it falls back to — add an edge// `Ledger -> Wallet "settle"`. That pins Ledger above Wallet inside `payments`,// so the corridor from `pci` up to Ledger now runs through Wallet, which is not// in `pci`. The frame stays where it is and Ledger gets a dashed `◇ PCI scope`// tab on its bottom edge instead, plus an `info` diagnostic saying the frame// could not reach it. Expect the tab to be the common outcome rather than the// exception: whether the corridor is clear depends on where the dependency flow// put the cards, which is not something you chose.//// The diagnostics panel also reports Ledger as an `info` for the model fact —// it belongs to more than one boundary. A fact worth seeing, not an error, the// same register as a node owned by two teams. That one is about the MODEL; the// reach diagnostic above is about THIS DRAWING.//// There is one case where a shared node moves frames: a boundary whose members// are ALL claimed by earlier ones has nothing to band, so rather than vanish it// takes one of its shared members — as long as the boundary it takes from keeps// another member. Not the case here: `pci` bands Card vault on its own, so it// needs nothing from `payments`.//// Boundaries group by meaning, and meanings overlap: Ledger is part of the// payments flow AND inside the PCI audit scope. Listing it in both is a normal// thing to write, not a mistake (see boundary-clusters.krs for the basics of// the axis). The whole membership survives a multi-file model too — a// `boundary` in an imported file and one here both count, rather than the// first one winning.//// `boundary` is core notation from `.krs language v2.0` (docs/spec/syntax.md// § Grouping the system view).
system Payments { label "Payments platform"
service Checkout { label "Checkout" } service Ledger { label "Ledger" } service Wallet { label "Wallet" } service CardVault { label "Card vault" } service Reporting { label "Reporting" }
database LedgerDB { label "Ledger DB" }
Checkout -> CardVault "tokenize" Checkout -> Ledger "record" Checkout -> Wallet "debit" Ledger -> LedgerDB "persist" Reporting -> LedgerDB "read"}
boundary payments { label "Payments" contains Checkout contains Ledger contains Wallet}
boundary pci { label "PCI scope" // Ledger is already in `payments` above. Both memberships are kept, and both // frames enclose it — this one reaches up to the row Ledger sits on. contains Ledger contains CardVault}Scoped declaration (boundary inside a node block)
Section titled “Scoped declaration (boundary inside a node block)”Drawn with Group by: Boundary. This is the root view; the boundary declared inside Checkout frames only Checkout’s drill-down view. Read the spec
// Scoped boundary — a `boundary` declared INSIDE a node block (#2036).//// Written inside a node block, a boundary is that layer's own concern: its// members are the declaring node's DIRECT CHILDREN (bare ids — siblings are// already unique), and its frame appears on the declaring node's canvas only.// Contrast boundary-clusters.krs, where a top-level boundary gathers nodes by// reference from anywhere and fragments across drill levels.//// Turn "Group by: Boundary" on and drill to see each declaration frame its own// canvas:// - `edge` (declared in the system block) frames Web + Mobile on the root view.// - `core` (declared inside Checkout) frames Ledger + Cart on Checkout's// drill-down view — and nowhere else. Reporting stays outside the frame.//// A same-named boundary in another scope is a DIFFERENT boundary (identity =// declaring scope + id): its frame, label, and collapse state are independent.//// `boundary` is core notation from `.krs language v2.0` (docs/spec/syntax.md// § Grouping the system view).
system Shop { label "Shop"
boundary edge { label "Edge services" contains Web contains Mobile }
service Web { label "Web BFF" } service Mobile { label "Mobile BFF" }
service Checkout { label "Checkout"
boundary core { label "Core domains" contains Ledger contains Cart }
domain Ledger { label "Ledger" } domain Cart { label "Cart" } domain Reporting { label "Reporting (unclustered)" } }
Web -> Checkout "order" Mobile -> Checkout "order"}tag / annotation / facet / boundary: which register says what
Section titled “tag / annotation / facet / boundary: which register says what”Drawn with the pci facet selected. Select the same facet from Facets in the app to get this diagram. Read the spec
// The four vocabulary registers, side by side (Issue #2177; `facet` and// `boundary` are core from `.krs language v2.0`).//// karasu has four ways to say something extra about an element, and they are// not interchangeable. Picking the wrong one is the most common modelling// mistake, so this file puts all four on one diagram://// tag [external] [index] [web] what the element IS — its archetype// annotation @deprecated @new where it is in its LIFECYCLE// facet facets pci, pii what externally-defined SET it is in// boundary boundary … contains how you want the VIEW grouped//// The test that separates them: could the element change without the model// changing? A database is a database whether or not PCI scope covers it — PCI// is imposed from outside, so it is a facet. `@deprecated` is a fact about this// model's timeline. `[index]` says what the store is for. And `boundary` is not// about the element at all; it is about how you want to look at the picture.//// Things worth trying, in this order://// 1. Select `pci` in the "Facets" selector. The members ring; everything else// dims. Notice what the tags did NOT do — `[external]` never grouped// anything, because it was never a membership. The Checkout → terminal// edge lights up on its own account: it carries card data, which is a// fact about the hop rather than about either end of it.// 2. Open "Membership overview" at the bottom of that menu. The list of// what is in `pci` is DERIVED from the `facets` lines below; nothing in// this file authors it, so it cannot be out of date.// 3. Switch "Group by" to `boundary` while `pci` is still selected. The// frames and the rings coexist — one is a layout, the other is a paint.// 4. Look at `tag-facet-registers.krs.style`. `[facets=pci]` styles by// membership, which is what an arbitrary `[pci]` tag selector used to be// abused for. In `.krs language v2.0` that tag rule matches nothing; this is the// replacement.
@import "tag-facet-registers.krs.style"
// ── facet: externally-defined sets ────────────────────────────────// The declaration carries the concern's metadata and nothing else. There is no// `contains` list — membership is written on the elements, so a rename never// means editing a distant list. Deliberately no predicates, ever: a facet says// WHICH things a policy covers, never what the policy says (ADR-832).
facet pci { label "Cardholder data" description "In scope for the annual PCI DSS assessment" link "https://example.com/policies/pci" "PCI policy"}
facet pii { label "Personal data" description "Holds or transits data identifying a natural person"}
// ── boundary: a grouping you draw over the view ───────────────────// Not a property of the elements. Two people can group the same system// differently and both be right, which is why this is by reference and why it// only draws under "Group by: boundary".
boundary money { label "Money path" contains Checkout contains Ledger}
boundary discovery { label "Discovery" contains Catalogue contains Search}
system Shop { label "Online shop"
user Customer [human] { label "Customer" }
// `[web]` is an archetype: it says what kind of client this is. It is not a // set anyone outside the architecture defined. client WebApp [web] { label "Web storefront" facets pii }
service Checkout { label "Checkout" description "Takes payment — sees both the buyer and the card" // Two facets at once. Multi-membership is a normal state, not a conflict: // an element can be in PCI scope and hold personal data. facets pci, pii }
service Accounts { label "Accounts" description "Profiles, addresses, consent records" facets pii }
service Catalogue { label "Catalogue" description "Products and prices. Holds nothing about anyone." }
// All three registers on one element, each saying a different kind of thing. service Search @new { label "Search" description "Query service over the catalogue" }
// `@deprecated` is lifecycle; `[external]` is archetype. Neither is // membership — the card processor is not "in" PCI scope, it IS the outside. service LegacyCardTerminal [external] @deprecated { label "In-store card terminal" description "Being retired once the new gateway lands" }
// `[index]` is an archetype tag: what the store is for. `facets pci` is the // regulation that happens to cover it. Same element, two different registers. database Ledger [index] { label "Payment ledger" facets pci }
database ProfileStore { label "Profile store" facets pii }
storage ProductImages { label "Product images" }
Customer -> WebApp "browses" WebApp -> Accounts "sign in" WebApp -> Checkout "pay" WebApp -> Catalogue "browse" Catalogue -> Search "index" Catalogue -> ProductImages "read" Accounts -> ProfileStore "read/write" Checkout -> Ledger "record"
// An edge can be in a facet too, and here it has to be. The card number // leaves the building on this hop, so PCI covers the FLOW — the terminal // outside is not in scope, and Checkout being in scope does not say that // this particular hop carries card data. Membership belongs on the edge. Checkout --> LegacyCardTerminal { label "settle" facets pci }}© 2026 Hiroki Kondo · Licensed underApache-2.0