@formicoidea/labre-framework-c4 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/actions.d.ts +179 -0
  2. package/dist/actions.js +375 -0
  3. package/dist/background.d.ts +77 -0
  4. package/dist/background.js +223 -0
  5. package/dist/commands.d.ts +4 -0
  6. package/dist/commands.js +221 -0
  7. package/dist/component.d.ts +192 -0
  8. package/dist/component.js +188 -0
  9. package/dist/consts.d.ts +331 -0
  10. package/dist/consts.js +384 -0
  11. package/dist/descriptor.d.ts +12 -0
  12. package/dist/descriptor.js +10 -0
  13. package/dist/effects.d.ts +9 -0
  14. package/dist/effects.js +6 -0
  15. package/dist/element-renderer.d.ts +18 -0
  16. package/dist/element-renderer.js +14 -0
  17. package/dist/element-view.d.ts +51 -0
  18. package/dist/element-view.js +146 -0
  19. package/dist/export.d.ts +184 -0
  20. package/dist/export.js +454 -0
  21. package/dist/index.d.ts +16 -0
  22. package/dist/index.js +52 -0
  23. package/dist/interchange.d.ts +74 -0
  24. package/dist/interchange.js +143 -0
  25. package/dist/legend.d.ts +37 -0
  26. package/dist/legend.js +123 -0
  27. package/dist/levels.d.ts +70 -0
  28. package/dist/levels.js +46 -0
  29. package/dist/morph.d.ts +89 -0
  30. package/dist/morph.js +229 -0
  31. package/dist/node/node-renderer.d.ts +6 -0
  32. package/dist/node/node-renderer.js +304 -0
  33. package/dist/node/node-view.d.ts +45 -0
  34. package/dist/node/node-view.js +80 -0
  35. package/dist/node/type-line-watcher.d.ts +70 -0
  36. package/dist/node/type-line-watcher.js +142 -0
  37. package/dist/presets.d.ts +84 -0
  38. package/dist/presets.js +149 -0
  39. package/dist/profiles.d.ts +2 -0
  40. package/dist/profiles.js +177 -0
  41. package/dist/roles.d.ts +116 -0
  42. package/dist/roles.js +303 -0
  43. package/dist/rules.d.ts +95 -0
  44. package/dist/rules.js +1261 -0
  45. package/dist/toolbar/c4-menu.d.ts +11 -0
  46. package/dist/toolbar/c4-menu.js +14 -0
  47. package/dist/toolbar/c4-senior-button.d.ts +19 -0
  48. package/dist/toolbar/c4-senior-button.js +23 -0
  49. package/dist/toolbar/config.d.ts +150 -0
  50. package/dist/toolbar/config.js +436 -0
  51. package/dist/toolbar/icons.d.ts +90 -0
  52. package/dist/toolbar/icons.js +157 -0
  53. package/dist/toolbar/senior-tool.d.ts +1 -0
  54. package/dist/toolbar/senior-tool.js +11 -0
  55. package/dist/translations.d.ts +18 -0
  56. package/dist/translations.js +42 -0
  57. package/dist/type-line.d.ts +175 -0
  58. package/dist/type-line.js +244 -0
  59. package/dist/view.d.ts +33 -0
  60. package/dist/view.js +148 -0
  61. package/package.json +34 -0
package/dist/rules.js ADDED
@@ -0,0 +1,1261 @@
1
+ import { C4_BOUNDARY_BACKGROUND } from './background.js';
2
+ import { C4_ROLE, C4_ROLES } from './roles.js';
3
+ /**
4
+ * C4 validation rules — the review checklist, as DATA.
5
+ *
6
+ * DATA owned by the framework, versioned per rule: the engine
7
+ * (`@labre/affine-block-surface`) knows how to evaluate a FAMILY, never a
8
+ * concrete rule, so adding a C4 rule is adding an entry to the array at the
9
+ * bottom of this file. Registered from the flag-gated `C4ViewExtension`, so
10
+ * switching the `c4` flag off removes the rules with the rest of the tooling —
11
+ * diagrams already drawn keep rendering, they simply stop being checked
12
+ * (`docs/adr/0009`).
13
+ *
14
+ * ## Where the rules come from
15
+ *
16
+ * C4 has no specification. What it has instead is Simon Brown's **diagram
17
+ * review checklist** (c4model.com), a short list of questions to ask of a
18
+ * finished diagram before handing it to somebody — and the notation's whole
19
+ * claim is that a diagram passing it can be read by a person who was not in the
20
+ * room. Four of its questions are answerable from the model alone and are the
21
+ * four this pack asks:
22
+ *
23
+ * - is every ELEMENT named? (`c4.unnamed-element`, which reads the `c4:title`
24
+ * text of a component group rather than any of the four levels)
25
+ * - does every RELATIONSHIP carry a label saying what it is for?
26
+ * (`c4.unlabeled-relationship`)
27
+ * - is every relationship one a reader can follow — between two things the
28
+ * model has, in one direction? (`c4.relationship-endpoints`,
29
+ * `c4.untyped-link`, and `c4.relationship-self-loop`, which is ours)
30
+ * - does every element earn its place on the sheet?
31
+ * (`c4.isolated-*`, `c4.homeless-component`)
32
+ *
33
+ * ## The fifth question, which is not the checklist's but the MODEL's
34
+ *
35
+ * Three rules — `c4.system-in-boundary`, `c4.container-in-container-boundary`
36
+ * and `c4.component-level-skip` — ask something the review checklist never
37
+ * phrases, because C4 says it one level further up, in the abstractions
38
+ * themselves (c4model.com/abstractions):
39
+ *
40
+ * > A software system is made up of one or more containers, each of which
41
+ * > contains one or more components.
42
+ *
43
+ * The four levels are therefore ZOOMS of one element, not four kinds of box, and
44
+ * the boundary is where the canvas says which zoom a part of the sheet is at.
45
+ * From which three things follow with no judgement of ours added: a software
46
+ * system drawn INSIDE a boundary is a mistyped container or a zoom that never
47
+ * happened (the boundary already IS a system or a container); a container drawn
48
+ * inside a CONTAINER boundary is that boundary, drawn inside itself; and a
49
+ * component framed only by a SYSTEM boundary has skipped the container level the
50
+ * model puts between them.
51
+ *
52
+ * None of the three could be asked before `roles.ts` split the boundary into
53
+ * `c4:system-boundary` and `c4:container-boundary`: "inside a boundary" was one
54
+ * undifferentiated fact, and a rule can only read the level off the frame if the
55
+ * frame says it.
56
+ *
57
+ * ## The sixth question, which is asked of the SHEET
58
+ *
59
+ * Two more rules — `c4.context-diagram-level` and `c4.container-diagram-level` —
60
+ * read the same statement one step out: C4's levels are not only zooms of an
61
+ * element, they are DIAGRAM TYPES, and each of the three the editor draws is
62
+ * defined by what appears on it (c4model.com, one page per diagram type).
63
+ *
64
+ * They are the first rules in this library whose subject is the sheet rather
65
+ * than an artefact, and they exist because no rule reading the drawing alone
66
+ * could see the level skip {@link componentLevelSkip} documents at length. The
67
+ * fact had to be declared, so the board now carries an optional `level` and the
68
+ * author sets it; a board that states none is a free sketch and neither rule
69
+ * evaluates a thing on it — which is every C4 diagram drawn before this slice.
70
+ *
71
+ * ## Where each rule's authority comes from, as DATA
72
+ *
73
+ * Every rule declares a {@link RuleProvenance}, and the split is **eleven
74
+ * `recommendation` / five `labre-convention`**. `standard` appears nowhere and
75
+ * never will: C4 has no published specification to cite a clause of, which is
76
+ * exactly why the checklist is the source — so the six that read it are
77
+ * `recommendation`, naming the method the way `wardley` and `ddd-context-map`
78
+ * name theirs. The three zoom rules are `recommendation` too, and cite the
79
+ * abstractions page rather than the checklist: what they indict is a statement
80
+ * of C4's own model. The two LEVEL rules cite a third page — the diagram types
81
+ * — for the same kind of reason: what each sheet shows is a definition C4
82
+ * publishes, not a question the checklist asks.
83
+ *
84
+ * The five that are OURS say so out loud, in the field and in their own
85
+ * comments: `c4.untyped-link` (a gesture of this canvas C4 never anticipated),
86
+ * `c4.relationship-self-loop`, `c4.homeless-component` (the MEMBERSHIP is C4's;
87
+ * requiring it to be drawn as a rectangle round the parts is ours),
88
+ * `c4.database-initiates` and `c4.person-in-boundary`.
89
+ *
90
+ * That is the whole point of promoting it to a field: an architecture review
91
+ * asked that a Labre convention never present itself as a norm violation, and
92
+ * a citation the reader can weigh is how the bubble keeps that promise.
93
+ *
94
+ * Provenance and SEVERITY are orthogonal, and this pack is where that shows:
95
+ * three of the five conventions are promoted by `c4.strict` and two are not.
96
+ * What decides is whether the diagram might honestly have meant it, never where
97
+ * the rule came from — see `profiles.ts`.
98
+ *
99
+ * ## Severity: the CROQUIS primes, so nearly everything is `audit`
100
+ *
101
+ * The same promise `wardley/rules.ts:30` and `bpmn/rules.ts` make, and one step
102
+ * further. Every rule here is declared `audit`, and the default profile —
103
+ * `c4.sketch` — keeps it there. A C4 diagram is drawn from the outside in: the
104
+ * boxes go down first, the arrows next, the words last, and for the whole of
105
+ * that a checklist is describing a drawing whose author already knows it is
106
+ * unfinished. `c4.strict` is the level somebody chooses when the diagram has
107
+ * become a deliverable, and it promotes ELEVEN of the sixteen to `warning` — the
108
+ * checklist half, the three zoom rules and the two level rules, none of which
109
+ * has a reading under which the drawing meant it (`profiles.ts`).
110
+ *
111
+ * `blocking-overridable` appears nowhere. NOTHING downstream implements a
112
+ * blocking level — no gesture is refused anywhere in this library — so shipping
113
+ * the value would be data claiming an effect that does not exist. Nothing in
114
+ * C4 would sit there even if it did: a checklist is a set of questions, not a
115
+ * set of prohibitions.
116
+ *
117
+ * ## What the whole file stays silent about
118
+ *
119
+ * A diagram drawn before the roles existed carries no role on anything, so it
120
+ * is never evaluated and never says a word (PRD principle 8). A board drawn
121
+ * before anybody added a boundary has no frame for the five boundary rules to be
122
+ * about, and all five are silent by construction.
123
+ *
124
+ * A boundary drawn before the ROLE SPLIT carries the parent `c4:boundary` and no
125
+ * child, and the split is what makes the difference legible: the two rules framed
126
+ * on the parent reach it exactly as they always did, and the two framed on
127
+ * `c4:container-boundary` find no frame of their own on such a board and say
128
+ * nothing. An old document gains no finding it did not already have — see
129
+ * {@link componentLevelSkip}, which is the whole of that argument.
130
+ *
131
+ * A board drawn before the LEVEL existed carries none, and the two level rules
132
+ * evaluate nothing on it — the same promise the boundary split made, one frame
133
+ * out, and the reason the field is optional with no default to read.
134
+ *
135
+ * ## Two questions this pack deliberately does NOT ask
136
+ *
137
+ * Not gaps in the engine — gaps in what a v1 C4 element can be asked. Each one
138
+ * is a rule the day the model carries the fact it would read. There were three;
139
+ * the middle one was **levels mixed on one board**, and it is now asked: the
140
+ * board declares its level and `c4.context-diagram-level` /
141
+ * `c4.container-diagram-level` judge what is drawn on it. That entry stayed on
142
+ * this list for as long as it did because the honest blocker was never the
143
+ * engine but the MODEL — "the BOARD carries no level", it used to say, "so
144
+ * nothing on it says which of the four sheets this is". The board carries one
145
+ * now, and the two remaining entries are still waiting for exactly that kind of
146
+ * fact:
147
+ *
148
+ * - **an empty boundary.** "A boundary round nothing is a claim about nothing"
149
+ * is a `role-count` question, and `role-count` counts ONE subject role per
150
+ * rule. The four levels are deliberately FLAT (`roles.ts` says why at
151
+ * length), so there is no single role meaning "any C4 element" for the rule
152
+ * to count — and four rules each saying "this boundary holds no component"
153
+ * would fire on the three perfectly ordinary boundaries holding one of the
154
+ * other kinds.
155
+ * - **the technology of a container or a component.** The checklist asks for it
156
+ * ("Java/Spring", "React SPA", "PostgreSQL") and the notation draws it as a
157
+ * second line under the name. The blocker that used to be here is GONE — the
158
+ * type line is its own element now, stamped `c4:type-line`, and
159
+ * `technologyOfTypeLine` reads the author's half back out of it. What stops
160
+ * the rule is no longer the model but the QUESTION: `label-presence` asks
161
+ * whether there are words, and a type line always has some (the kind's own
162
+ * word is seeded into it). "Has a technology" means "has more than the word
163
+ * the tool wrote", which is a predicate on the content — a family this engine
164
+ * does not have, and a bad first reason to invent one. Worth revisiting when
165
+ * a second framework wants the same shape of question.
166
+ */
167
+ /**
168
+ * Every C4 role that is an ELEMENT of the model — the four levels, and
169
+ * therefore the whole alphabet a relationship is read against.
170
+ *
171
+ * `c4:database` is absent and covered: it is a `c4:container` by declaration
172
+ * (`roles.ts`), so `roleIsA` reaches it from the container entry, and `mobile`
173
+ * and `browser` are containers with no role of their own at all. The two frames
174
+ * — the board and the boundary — are absent and MEANT to be: a relationship
175
+ * dragged onto the sheet, or onto the dashed rectangle round part of it, is
176
+ * somebody pointing at something, and pointing at things is what a whiteboard
177
+ * is for.
178
+ */
179
+ const C4_ELEMENT_ROLES = [
180
+ C4_ROLE.person,
181
+ C4_ROLE.system,
182
+ C4_ROLE.container,
183
+ C4_ROLE.component,
184
+ ];
185
+ /**
186
+ * Every ordered pair of the four levels — the ALPHABET, and nothing more.
187
+ *
188
+ * Not a grammar: this table sanctions all sixteen sentences, person → person
189
+ * included. It exists so {@link untypedLink} can say "between two C4 elements"
190
+ * — which is the only thing `flagNeutral` reads a matrix FOR — without also
191
+ * inheriting a judgement that belongs to {@link relationshipEndpoints}.
192
+ *
193
+ * ## Why the two tables are not one, which they were for a day
194
+ *
195
+ * `relation-endpoints` raises an off-matrix finding for every rule declaring a
196
+ * matrix, so two rules sharing one grammar report the same wrong sentence
197
+ * TWICE, with two brackets and two suggestions for one gesture to fix. BPMN
198
+ * never met this because its neutral-link rule reuses a matrix on which
199
+ * off-matrix is structurally unreachable (one triplet, one role). C4's grammar
200
+ * has exactly one removal, so it is reachable, and the alphabet has to be
201
+ * declared separately for the neutral rule to stay the single-verdict rule its
202
+ * own comment promises.
203
+ */
204
+ export const C4_ELEMENT_MATRIX = C4_ELEMENT_ROLES.flatMap(source => C4_ELEMENT_ROLES.map(target => ({
205
+ source,
206
+ edge: C4_ROLE.relationship,
207
+ target,
208
+ })));
209
+ /**
210
+ * The sanctioned sentences of a relationship: **anything uses anything, except
211
+ * a person using a person.**
212
+ *
213
+ * C4 is deliberately permissive here and the matrix says so. A person uses a
214
+ * system; a container reads from a database; a component calls a system. The
215
+ * notation puts no level barrier on an arrow — a C4 diagram is drawn at one
216
+ * level, so the pairs that would be odd are odd because of the SHEET they are
217
+ * on, which is a judgement this pack cannot make (see the header on D3).
218
+ *
219
+ * The one sentence C4 does not have is **person → person**. Two people talking
220
+ * to each other is a true and important thing about an organisation, and it is
221
+ * not software: the model has no drawing for it, and a diagram that shows one
222
+ * is a diagram whose author reached for the wrong canvas. It is the only
223
+ * removal, and it is why the grammar is the alphabet minus one line rather than
224
+ * fifteen lines written out — the rule is "all of them but that one", and the
225
+ * data reads best when it says what the rule says.
226
+ *
227
+ * Exported so a test asserts THIS table rather than a copy of it.
228
+ */
229
+ export const C4_RELATIONSHIP_MATRIX = C4_ELEMENT_MATRIX.filter(triplet => !(triplet.source === C4_ROLE.person && triplet.target === C4_ROLE.person));
230
+ /* ── Naming: does the drawing say anything at all? ─────────────────────── */
231
+ /**
232
+ * **C1** — an arrow nobody has labelled.
233
+ *
234
+ * The checklist item C4 practitioners quote most often, and the one the
235
+ * notation is least forgiving about: an unlabelled arrow between two boxes says
236
+ * only "these two are connected", which the reader could already see. What a
237
+ * relationship is FOR — "sends the order to", "reads the customer file from",
238
+ * "authenticates against" — is the whole content of the line, and the role's
239
+ * own default verb is deliberately the weakest one in the pack ("uses") so that
240
+ * a diagram never has words put in its mouth by the tool (`roles.ts`).
241
+ *
242
+ * ## `on-demand`, and `audit`
243
+ *
244
+ * Both softenings are the ones {@link unnamedPerson} and its three siblings
245
+ * make, for the same reasons: a relationship is CREATED unlabelled — the
246
+ * gesture drops a line and the author types on it a second later — so a
247
+ * real-time rule would bracket every arrow the instant it appeared, which is
248
+ * not validation but arguing with the act of drawing (PRD principle 3). "Four
249
+ * arrows are unlabelled" is a sentence for the check-up panel, once, when
250
+ * somebody asks whether the diagram is finished.
251
+ *
252
+ * The `label-presence` family reads the subject's OWN `text`, which for a
253
+ * relationship is the connector's label — where C4 puts the words — so the
254
+ * question can be asked at all.
255
+ */
256
+ const unlabeledRelationship = {
257
+ id: 'c4.unlabeled-relationship',
258
+ framework: 'c4',
259
+ family: 'label-presence',
260
+ severity: 'audit',
261
+ appliesTo: C4_ROLE.relationship,
262
+ roles: C4_ROLES,
263
+ messageKey: 'com.labre.c4.validation.unlabeled-relationship',
264
+ messageFallback: 'This relationship has no label.',
265
+ suggestionKey: 'com.labre.c4.validation.unlabeled-relationship.suggestion',
266
+ suggestionFallback: 'Write what one element does with the other — "Sends the order to", "Reads the customer file from". An unlabelled arrow says only that the two are connected, which the reader could already see.',
267
+ version: 1,
268
+ provenance: {
269
+ source: 'recommendation',
270
+ reference: 'C4 model — diagram review checklist (c4model.com): every relationship carries a label saying what it is for',
271
+ },
272
+ // Explicitly on-demand, and it stays explicit: `moment: undefined` means
273
+ // REALTIME, and the engine watches `text` for exactly this family when a
274
+ // real-time rule of it is registered. Dropping this line would hand the
275
+ // drawing path a debounced re-evaluation per keystroke.
276
+ moment: 'on-demand',
277
+ // Attribution only — the count reads no geometry — so an arbitration made on
278
+ // one board covers that board and no other.
279
+ backgroundRole: C4_ROLE.board,
280
+ label: { present: true },
281
+ };
282
+ /**
283
+ * **C2** — an element nobody has named.
284
+ *
285
+ * ONE rule for all nine artefacts, and it reads the `c4:title` TEXT rather than
286
+ * any of the four levels.
287
+ *
288
+ * ## Why it is one rule now, where it was four
289
+ *
290
+ * It was four — `c4.unnamed-{person,system,container,component}` — because
291
+ * `label-presence` names one `appliesTo` role, the four levels are flat by
292
+ * declaration, and a shape's name lived in the shape's own inner text. Every
293
+ * one of those premises moved when the component became a GROUP: the shape
294
+ * carries **no text at all**, and an element's name is a text child stamped
295
+ * `c4:title` (`roles.ts` says so at length, and instructs a naming rule to read
296
+ * exactly that role).
297
+ *
298
+ * So there is now a single role meaning "the name of a C4 element", and four
299
+ * rules asking the same question of it would be one question asked four times.
300
+ * The per-level wording goes with them — a title is a title at every level, and
301
+ * the rule can no longer tell a person's from a container's without walking the
302
+ * group, which is not this family's business.
303
+ *
304
+ * ## What it actually fires on, which is narrower than it sounds
305
+ *
306
+ * A fresh node is created with its kind's own label already in the title —
307
+ * `Person`, `Web app`, `API Application` (`actions.ts` seeds `NODE_LABEL[kind]`,
308
+ * a name and a prompt at once). `label-presence` sees words and says nothing.
309
+ * The rule therefore fires on exactly one thing: **a title an author has
310
+ * emptied.** That is a good deal quieter than the four rules it replaces, which
311
+ * fired on every freshly dropped box until somebody typed — and it is the
312
+ * honest reading, because a box reading "Person" is a box whose author has not
313
+ * finished, not a box with no name.
314
+ *
315
+ * ## The known limit: a DELETED title is silence
316
+ *
317
+ * If the title element is removed outright rather than emptied, there is no
318
+ * `c4:title` on the board for this rule to be about, and it says nothing. The
319
+ * component is then genuinely nameless and the check-up will not mention it.
320
+ *
321
+ * That is a real hole and it is named rather than papered over. Closing it means
322
+ * asking a different question — "does this SHAPE have a title among its group's
323
+ * children?" — which is a rule about group membership, not about a label's
324
+ * presence, and no family expresses it today. `c4ComponentSiblings` is where the
325
+ * answer would come from when one does. Until then the drawn diagram is judged
326
+ * and the missing element is not, which is the same direction every other
327
+ * silence in this file leans.
328
+ */
329
+ const unnamedElement = {
330
+ id: 'c4.unnamed-element',
331
+ framework: 'c4',
332
+ family: 'label-presence',
333
+ severity: 'audit',
334
+ // The TITLE, not the artefact: the shape carries no text since the component
335
+ // became a group, so a rule written on `c4:person` would read nothing on
336
+ // every element on the board and report every one of them unnamed.
337
+ appliesTo: C4_ROLE.title,
338
+ roles: C4_ROLES,
339
+ messageKey: 'com.labre.c4.validation.unnamed-element',
340
+ messageFallback: 'This element has no name.',
341
+ suggestionKey: 'com.labre.c4.validation.unnamed-element.suggestion',
342
+ suggestionFallback: 'Write the name a reader outside the room would recognise — "Personal banking customer" for a person, "API Application" for a container, "Sign In Controller" for a component. The type line under it already says what kind of thing it is, so the name is free to say which one.',
343
+ version: 1,
344
+ provenance: {
345
+ source: 'recommendation',
346
+ reference: 'C4 model — diagram review checklist (c4model.com): every element has a name',
347
+ },
348
+ // See `unlabeledRelationship`: naming is what a user does by TYPING, so the
349
+ // check belongs to the moment somebody asks whether the diagram is done
350
+ // rather than to every keystroke.
351
+ moment: 'on-demand',
352
+ backgroundRole: C4_ROLE.board,
353
+ label: { present: true },
354
+ };
355
+ /* ── Grammar: what an arrow may run between ────────────────────────────── */
356
+ /**
357
+ * **C3** — a plain connector between two C4 elements is a relationship nobody
358
+ * typed.
359
+ *
360
+ * The gap every other rule in this file falls through, and BPMN's `B4`
361
+ * one framework over. The C4 relationship tool stamps a role; quick-connect and
362
+ * auto-complete do not, so releasing the canvas' own link gesture between two
363
+ * boxes produces a connector carrying NO role — which says nothing to the
364
+ * grammar, nothing to the degree counts, and nothing to the naming check. A
365
+ * diagram joined that way LOOKS connected and validates as if nobody had joined
366
+ * anything: every element is isolated, no arrow is unlabelled, and the picture
367
+ * on screen shows arrows.
368
+ *
369
+ * So the rule is not really about the connector: it is about the verdicts that
370
+ * go quiet behind it.
371
+ *
372
+ * ## Why it declares a matrix it never judges anything against
373
+ *
374
+ * `flagNeutral` reads the rule's own ALPHABET to decide which role-less links
375
+ * it may presume were meant as relationships, so the matrix is how this rule
376
+ * says "between two C4 elements" — and, by saying only that, how it stays
377
+ * silent about everything else. A plain connector onto the BOARD, onto a
378
+ * boundary, onto a sticky note, onto a shape somebody dropped to think with is
379
+ * an annotation and none of C4's business.
380
+ *
381
+ * Nothing else in the declaration fires. The matrix is
382
+ * {@link C4_ELEMENT_MATRIX} — every ordered pair of the four levels, person →
383
+ * person included — so off-matrix is structurally UNREACHABLE here and the one
384
+ * sentence C4 forbids stays {@link relationshipEndpoints}' finding alone. That
385
+ * separation is the whole reason the alphabet and the grammar are two tables;
386
+ * sharing one made this rule report the same wrong arrow a second time.
387
+ * `forbidSelfLoop` is absent for the same reason. This rule raises exactly one
388
+ * kind of finding.
389
+ */
390
+ const untypedLink = {
391
+ id: 'c4.untyped-link',
392
+ framework: 'c4',
393
+ family: 'relation-endpoints',
394
+ severity: 'audit',
395
+ roles: C4_ROLES,
396
+ // The rule's own words are never read: `flagNeutral` is the only verdict it
397
+ // can reach, and it carries its own. Declared all the same, because the shape
398
+ // requires them and a rule with no sentence is a rule nobody can review.
399
+ messageKey: 'com.labre.c4.validation.untyped-link',
400
+ messageFallback: 'This link between two C4 elements says nothing the model records.',
401
+ suggestionKey: 'com.labre.c4.validation.untyped-link.suggestion',
402
+ suggestionFallback: 'Draw it again with the relationship tool. A plain connector is on the diagram and absent from the model, so nothing can be said about what it means or which way round it reads.',
403
+ version: 1,
404
+ provenance: {
405
+ source: 'labre-convention',
406
+ reference: 'Labre convention — a role-less connector is a gesture of this canvas, not an artefact of the notation',
407
+ },
408
+ backgroundRole: C4_ROLE.board,
409
+ endpoints: {
410
+ edgeRole: C4_ROLE.relationship,
411
+ // The ALPHABET, and nothing else — see the header. Built from the same four
412
+ // roles C7's grammar is, so the two can never disagree about what "a C4
413
+ // element" is, and permissive enough that this rule judges no sentence.
414
+ allowed: C4_ELEMENT_MATRIX,
415
+ flagNeutral: {
416
+ messageKey: 'com.labre.c4.validation.untyped-link.neutral',
417
+ messageFallback: 'These two elements are joined by an untyped link.',
418
+ suggestionKey: 'com.labre.c4.validation.untyped-link.neutral.suggestion',
419
+ suggestionFallback: 'The diagram shows an arrow and the model holds none — nothing uses anything here. Draw it again with the relationship tool so it can carry a label and a direction.',
420
+ },
421
+ },
422
+ };
423
+ /**
424
+ * **C4** — a relationship runs between two things the model has.
425
+ *
426
+ * The grammar, and it is short because C4's is: the matrix sanctions every
427
+ * ordered pair of the four levels but person → person (see
428
+ * {@link C4_RELATIONSHIP_MATRIX} for why that one is out).
429
+ *
430
+ * ## Deliberately NOT `forbidDuplicate`
431
+ *
432
+ * Two arrows between the same two boxes going the same way is exactly how C4
433
+ * says a thing is used for two different reasons — "Reads from" and "Writes
434
+ * to", drawn separately because each carries its own label. That is the
435
+ * notation working, and a duplicate check would indict it. (The Context Mapping
436
+ * call is the opposite one, and for the opposite reason: there the same pattern
437
+ * twice is a claim made twice.)
438
+ *
439
+ * ## Off-matrix is REACHABLE here, unlike in most packs
440
+ *
441
+ * The single removal gives the rule something to say: a relationship drawn from
442
+ * one person to another is on the matrix's alphabet and off its table, so it is
443
+ * a finding rather than silence. Everything else that could be an end — the
444
+ * board, a boundary, a sticky note, a legend glyph — is outside the alphabet,
445
+ * and an edge with an end outside the alphabet takes the whole link out of the
446
+ * conversation.
447
+ *
448
+ * ## The self-loop is NOT here, and that is the point
449
+ *
450
+ * It was, until {@link relationshipSelfLoop} split it out. The two clauses have
451
+ * different AUTHORITY — the matrix is the C4 model speaking, the loop is our
452
+ * house reading — and one rule cannot honestly answer "where does this come
453
+ * from" twice. See that rule's own comment.
454
+ */
455
+ const relationshipEndpoints = {
456
+ id: 'c4.relationship-endpoints',
457
+ framework: 'c4',
458
+ family: 'relation-endpoints',
459
+ severity: 'audit',
460
+ // No `appliesTo`: the subject is a RELATION, and the role that names it is
461
+ // declared where the family reads it.
462
+ roles: C4_ROLES,
463
+ messageKey: 'com.labre.c4.validation.relationship-endpoints',
464
+ messageFallback: 'This relationship runs between two people.',
465
+ suggestionKey: 'com.labre.c4.validation.relationship-endpoints.suggestion',
466
+ suggestionFallback: 'C4 draws how software is used, so it has no arrow for two people talking to each other. Draw what they each use instead — or say it on a different board, where the model is people.',
467
+ version: 1,
468
+ provenance: {
469
+ source: 'recommendation',
470
+ reference: 'C4 model — the model is software and the people who use it; there is no notation for two people talking to each other',
471
+ },
472
+ backgroundRole: C4_ROLE.board,
473
+ endpoints: {
474
+ edgeRole: C4_ROLE.relationship,
475
+ allowed: C4_RELATIONSHIP_MATRIX,
476
+ // No `forbidSelfLoop`: that clause is `c4.relationship-self-loop` now, and
477
+ // its absence here is what lets exactly one of the two indict a given loop.
478
+ },
479
+ };
480
+ /**
481
+ * **C5** — a relationship that loops back onto the element it leaves.
482
+ *
483
+ * OURS, not the C4 model's, and split out of {@link relationshipEndpoints} for
484
+ * exactly that reason. The matrix restates something C4 says — the notation has
485
+ * no arrow for two people talking — while nothing in the model or the review
486
+ * checklist forbids an element from being drawn as using itself. Shipping both
487
+ * under one id meant one rule answering "where does this come from" two
488
+ * different ways, which is the "conventions presented as norm violations" trap;
489
+ * the same split BPMN made between its sequence-flow grammar and its own
490
+ * no-self-loop house style.
491
+ *
492
+ * ## What it costs a reader, which is why we keep it at all
493
+ *
494
+ * A relationship whose two ends are the same box says an element uses itself.
495
+ * On a C4 diagram that is never information: at every level, what a thing does
496
+ * internally is what the NEXT level down is for — a system that calls itself is
497
+ * a container diagram waiting to be drawn, and a container that calls itself is
498
+ * a component diagram. The arrow is the author noticing something real and
499
+ * drawing it on the wrong sheet.
500
+ *
501
+ * ## Why the two rules compose safely
502
+ *
503
+ * The ALPHABET GATE, before anything else: an edge with an end outside the four
504
+ * element roles is dropped before either the self-loop test or the matrix test
505
+ * is reached, so a relationship looped onto a boundary or a sticky note is
506
+ * silence from both rules. Past the gate, the family tests the self-loop FIRST
507
+ * and `continue`s unconditionally — the `continue` sits outside the
508
+ * `forbidSelfLoop` guard — so a loop never reaches a matrix at all. C7 declares
509
+ * no `forbidSelfLoop` and therefore says nothing about a loop; this rule
510
+ * declares the permissive ALPHABET rather than C7's grammar, so it has no
511
+ * sentence to judge and no verdict but the loop.
512
+ *
513
+ * That last point is not a detail, and the test suite caught it being wrong:
514
+ * BPMN's split rule safely carries its sibling's matrix because BPMN's matrix
515
+ * sanctions everything in its own alphabet, making off-matrix unreachable.
516
+ * C4's does not — person → person is a genuine removal — so a second rule
517
+ * carrying the grammar reports it a second time on every NON-loop relationship.
518
+ * In this pack exactly one rule may ever hold {@link C4_RELATIONSHIP_MATRIX}.
519
+ *
520
+ * The consequence worth stating, because it is the one a reviewer will ask
521
+ * about: a person looped onto THEMSELVES raises this rule ALONE, not also C7,
522
+ * even though person → person is the one sentence off C7's matrix. The engine
523
+ * never asks the matrix about a self-loop. That is a deliberate reading — the
524
+ * mistake the author made is the loop, and telling them a person may not use a
525
+ * person would be answering a question they did not ask.
526
+ *
527
+ * It carries no `selfLoop` block: with the matrix unreachable here, the rule's
528
+ * own message IS the self-loop message and the family falls back to it.
529
+ */
530
+ const relationshipSelfLoop = {
531
+ id: 'c4.relationship-self-loop',
532
+ framework: 'c4',
533
+ family: 'relation-endpoints',
534
+ severity: 'audit',
535
+ roles: C4_ROLES,
536
+ messageKey: 'com.labre.c4.validation.relationship-self-loop',
537
+ messageFallback: 'This relationship loops back onto the element it leaves.',
538
+ suggestionKey: 'com.labre.c4.validation.relationship-self-loop.suggestion',
539
+ suggestionFallback: 'What an element does inside itself is what the next diagram down is for: zoom into it and draw the parts. On this sheet the loop tells the reader nothing.',
540
+ version: 1,
541
+ provenance: {
542
+ source: 'labre-convention',
543
+ reference: 'Labre house style — C4 states no prohibition; what an element does inside itself is the next diagram down',
544
+ },
545
+ backgroundRole: C4_ROLE.board,
546
+ endpoints: {
547
+ edgeRole: C4_ROLE.relationship,
548
+ // {@link C4_ELEMENT_MATRIX}, NOT C7's grammar — the alphabet, so that a
549
+ // relationship looped onto a frame or a sticky note stays outside the
550
+ // conversation, and permissive so that this rule judges no sentence.
551
+ //
552
+ // Carrying C7's table here would report person → person a second time: the
553
+ // matrix is only unreachable for the LOOPS this rule indicts, and every
554
+ // other relationship on the board still walks past it to the off-matrix
555
+ // test. That is the same trap {@link untypedLink} documents, and it is
556
+ // structural in C4: the grammar has a removal in it, so exactly one rule
557
+ // may ever carry it.
558
+ allowed: C4_ELEMENT_MATRIX,
559
+ forbidSelfLoop: true,
560
+ },
561
+ };
562
+ /* ── Degree: does the element earn its place on the sheet? ─────────────── */
563
+ /**
564
+ * The three isolation rules, one per connectable level, built from one
565
+ * description.
566
+ *
567
+ * `edge-degree` names ONE subject role, and the levels are flat, so this is the
568
+ * same three-or-four-rule shape {@link namingRule} explains — and the same free
569
+ * coverage: `c4:container` reaches the database, the mobile app and the
570
+ * single-page app.
571
+ *
572
+ * ## The floor is DISJUNCTIVE, and that is the whole rule
573
+ *
574
+ * `eitherMin: 1` — at least one relationship on ONE side. The four ordinary
575
+ * bounds cannot say it: `minIn: 1` indicts every element at the top of a
576
+ * diagram (a person uses things and is used by nothing), `minOut: 1` every
577
+ * element at the bottom (a database is written to and calls nobody), and both
578
+ * together indict a conformant diagram from end to end. What C4 asks is that a
579
+ * reader can see WHY the box is on the sheet, and one arrow either way answers
580
+ * it.
581
+ *
582
+ * ## `audit` at BOTH levels, and the person is out
583
+ *
584
+ * An unconnected box is a diagram that is not finished, not a diagram that is
585
+ * wrong — the boxes go down before the arrows, always — so the finding belongs
586
+ * to the panel at every level of requirement. And the PERSON has no rule of its
587
+ * own at all: dropping the actors on the sheet first and connecting them last
588
+ * is how a context diagram gets drawn, and a persona parked on the side while
589
+ * the author thinks is the single most common intermediate state there is.
590
+ */
591
+ function isolationRule(level, role, message) {
592
+ return {
593
+ id: `c4.isolated-${level}`,
594
+ framework: 'c4',
595
+ family: 'edge-degree',
596
+ severity: 'audit',
597
+ appliesTo: role,
598
+ roles: C4_ROLES,
599
+ messageKey: `com.labre.c4.validation.isolated-${level}`,
600
+ messageFallback: message,
601
+ suggestionKey: `com.labre.c4.validation.isolated-${level}.suggestion`,
602
+ suggestionFallback: 'Draw the relationship that puts it in the picture — what uses it, or what it uses. An element nothing touches leaves the reader unable to see why it is on the diagram.',
603
+ version: 1,
604
+ // One citation for all three, because they are one requirement written on
605
+ // three roles the vocabulary keeps flat — see the header.
606
+ provenance: {
607
+ source: 'recommendation',
608
+ reference: 'C4 model — diagram review checklist (c4model.com): a reader can see why every element is on the diagram',
609
+ },
610
+ backgroundRole: C4_ROLE.board,
611
+ degree: {
612
+ edgeRole: C4_ROLE.relationship,
613
+ // One relationship on ONE side. See the header on why no conjunction of
614
+ // the per-direction bounds expresses this.
615
+ eitherMin: 1,
616
+ },
617
+ };
618
+ }
619
+ /** **C6** — a software system nothing reaches and that reaches nothing. */
620
+ const isolatedSystem = isolationRule('system', C4_ROLE.system, 'Nothing connects this software system to the rest of the diagram.');
621
+ /** **C7** — a container nothing reaches and that reaches nothing. */
622
+ const isolatedContainer = isolationRule('container', C4_ROLE.container, 'Nothing connects this container to the rest of the diagram.');
623
+ /** **C8** — a component nothing reaches and that reaches nothing. */
624
+ const isolatedComponent = isolationRule('component', C4_ROLE.component, 'Nothing connects this component to the rest of the diagram.');
625
+ /**
626
+ * **C9** — a data store that calls somebody.
627
+ *
628
+ * OURS, not the checklist's, and an idiom rather than a law — which is why it
629
+ * is `audit` at both levels and says so in its own sentence.
630
+ *
631
+ * A C4 diagram is read as a set of sentences, and the one a database is the
632
+ * subject of almost never exists: containers read from and write to the store,
633
+ * and the store sits there. An arrow LEAVING a cylinder is, nine times out of
634
+ * ten, a hand that dragged from the wrong end — the two boxes are right, the
635
+ * label is right, and the sentence is backwards. The tenth time it is a
636
+ * replicating database or a store with a change-feed, which is a real thing to
637
+ * mean and which this rule deliberately reports quietly rather than refuses.
638
+ *
639
+ * `maxOut: 0`, on `c4:database` — and on nothing else. It cannot be written on
640
+ * `c4:container`, which would indict every application on the board.
641
+ */
642
+ const databaseInitiates = {
643
+ id: 'c4.database-initiates',
644
+ framework: 'c4',
645
+ family: 'edge-degree',
646
+ severity: 'audit',
647
+ appliesTo: C4_ROLE.database,
648
+ roles: C4_ROLES,
649
+ messageKey: 'com.labre.c4.validation.database-initiates',
650
+ messageFallback: 'A relationship leaves this data store.',
651
+ suggestionKey: 'com.labre.c4.validation.database-initiates.suggestion',
652
+ suggestionFallback: 'Containers read from and write to a store; a store usually calls nobody, so the arrow is often drawn from the wrong end. Turn it round — unless this really is a store that pushes, in which case say so in the label.',
653
+ version: 1,
654
+ provenance: {
655
+ source: 'labre-convention',
656
+ reference: "Labre convention — a reading idiom; C4 says nothing about which end of a store's arrow is which",
657
+ },
658
+ backgroundRole: C4_ROLE.board,
659
+ degree: {
660
+ edgeRole: C4_ROLE.relationship,
661
+ maxOut: 0,
662
+ },
663
+ };
664
+ /* ── Membership: which frame does the element belong to? ───────────────── */
665
+ /**
666
+ * **C10** — a component drawn outside any boundary.
667
+ *
668
+ * A component is the one level of C4 that means nothing on its own: it is a
669
+ * part OF a container, and the container it is part of is drawn on the canvas
670
+ * as the boundary round it. A component floating beside every boundary belongs
671
+ * to nothing the diagram names, so the reader cannot say what it is inside.
672
+ *
673
+ * ## Why it stays framed on the PARENT role, where it might have been retargeted
674
+ *
675
+ * The obvious move, once the boundary split in two, was to point this rule at
676
+ * `c4:container-boundary` — "a component belongs inside a CONTAINER boundary" —
677
+ * and be done in one rule. It is not taken, and the reason is old documents.
678
+ *
679
+ * `backgroundsOf` matches a frame by `roleIsA`, and a boundary stamped with the
680
+ * PARENT role is not a `c4:container-boundary`: descent runs from child to
681
+ * ancestor, never the other way. So on a diagram drawn before the split — every
682
+ * C4 board that exists today — a retargeted rule would find no frame of its own,
683
+ * and `element-in-background` answers a frameless board with silence. The rule
684
+ * would not fire wrongly; it would stop firing at all, and a check the user has
685
+ * had since the pack shipped would disappear from every document already drawn
686
+ * without anybody being told. (One board is worse than silent: draw a single new
687
+ * container boundary on an old diagram and every component sitting in the old
688
+ * frames becomes homeless at once.)
689
+ *
690
+ * So the requirement is split by what each half actually claims. THIS rule keeps
691
+ * the weaker, older claim — a component must be framed by SOMETHING — which is
692
+ * our drawing convention and reads the same on every document ever saved. The
693
+ * sharper claim, that the something must be a container, is C4's own model and is
694
+ * {@link componentLevelSkip}, framed on the child role and therefore silent on
695
+ * exactly the documents that never said which level their frames were at.
696
+ *
697
+ * The two compose, and the one seam worth stating: a component drawn outside
698
+ * EVERY boundary on a board that has a container boundary raises both — one
699
+ * saying it belongs to nothing, the other that no container claims it. That is
700
+ * redundant, not contradictory, and suppressing it would need a family that can
701
+ * ask "inside A but not inside B", which none of the eight expresses. Two
702
+ * remarks about one box is the honest price of the compatibility above.
703
+ *
704
+ * ## Containment, and the silence around it
705
+ *
706
+ * The subject must be fully inside SOME element carrying `c4:boundary` — the
707
+ * parent role, so both of its children and every pre-split boundary count — and
708
+ * the rule is silent whenever there is none on the board at all — a component
709
+ * diagram sketched before anybody drew the container frame is a sketch, and so
710
+ * is one drawn before the role existed. That silence is what keeps this from
711
+ * being the rule that lights a whole board up on its first minute.
712
+ *
713
+ * ## Where the finding lands, and which level judges it
714
+ *
715
+ * `element-in-background` attributes the finding to the BOUNDARY — that is the
716
+ * frame the question is asked about — and the boundary carries no picker of its
717
+ * own: **the board alone arbitrates the checklist** (PO, 28/08/2026). One
718
+ * diagram, one level of requirement, one place to set it.
719
+ *
720
+ * The two are reconciled in the engine rather than in the toolbar:
721
+ * `inheritChosenProfiles` makes a frame naming no profile inherit the innermost
722
+ * containing frame's choice, so a boundary drawn on a board set to Review
723
+ * checklist is itself on Review checklist, and this rule hardens with the other
724
+ * eight. Nothing is declared here to make that happen — the inheritance reads
725
+ * the containment the diagram already shows.
726
+ */
727
+ const homelessComponent = {
728
+ id: 'c4.homeless-component',
729
+ framework: 'c4',
730
+ family: 'element-in-background',
731
+ severity: 'audit',
732
+ appliesTo: C4_ROLE.component,
733
+ roles: C4_ROLES,
734
+ messageKey: 'com.labre.c4.validation.homeless-component',
735
+ messageFallback: 'This component sits outside every boundary.',
736
+ suggestionKey: 'com.labre.c4.validation.homeless-component.suggestion',
737
+ suggestionFallback: 'A component is a part of a container, and the container is the boundary drawn round it. Move it inside the one it belongs to, or draw the boundary that says which container this is.',
738
+ version: 1,
739
+ // The MEMBERSHIP is C4's ("a component is part of a container"); requiring it
740
+ // to be drawn as a rectangle round the parts is ours, and the rule reports
741
+ // the drawing. Same call `context-map.context-off-board` makes.
742
+ provenance: {
743
+ source: 'labre-convention',
744
+ reference: 'Labre convention — membership on this canvas, not a C4 rule',
745
+ },
746
+ // The frame, and the whole question. No `background` declaration is carried:
747
+ // `element-in-background` measures against the element BOX and reads no
748
+ // margin, so a geometry declaration here would be data nothing reads.
749
+ backgroundRole: C4_ROLE.boundary,
750
+ };
751
+ /**
752
+ * **C11** — a person drawn inside a boundary.
753
+ *
754
+ * OURS, like {@link databaseInitiates}, and `audit` at both levels for the same
755
+ * reason. A boundary says "these things are parts of one system, or of one
756
+ * container". A person is never a part of the software: they are who USES it,
757
+ * which is exactly the distinction the boundary exists to draw — a context
758
+ * diagram whose actors are inside the box has erased the only line it had.
759
+ *
760
+ * ## The family is `element-in-zone`, and why
761
+ *
762
+ * The question is "is this element OUTSIDE that frame", and
763
+ * `element-in-background` cannot ask it: it has one polarity, INSIDE, with no
764
+ * `expect` to turn round. `element-in-zone` has the polarity, and the boundary
765
+ * declares exactly one zone covering its whole plot (`background.ts` — it
766
+ * carries the boundary's name and paints nothing), so citing that zone is
767
+ * citing the inside of the boundary. Nothing is restated: the geometry comes
768
+ * from the declaration the renderer paints from, so a boundary that is moved,
769
+ * resized or re-margined moves this rule with it.
770
+ *
771
+ * ## What it stays silent about, and why that is the right amount
772
+ *
773
+ * `element-in-zone` judges a subject only against the frame that CONTAINS it,
774
+ * so a person straddling the dashed edge — half in, half out — raises nothing:
775
+ * only a person drawn wholly within a boundary is a person the author has put
776
+ * inside the system. A person on a board with no boundary at all is silence
777
+ * twice over. Both are the drawing hand being left alone.
778
+ *
779
+ * ## Which level judges it
780
+ *
781
+ * The other rule framed against the boundary, and the same answer: the finding
782
+ * lands on the boundary, the boundary carries no picker, and it inherits the
783
+ * board's choice through the engine's `inheritChosenProfiles`. This one stays
784
+ * `audit` at both levels anyway, so the inheritance changes nothing it reports
785
+ * today — it is what keeps that a decision of the profile table rather than an
786
+ * accident of where the finding happened to be anchored.
787
+ */
788
+ const personInBoundary = {
789
+ id: 'c4.person-in-boundary',
790
+ framework: 'c4',
791
+ family: 'element-in-zone',
792
+ severity: 'audit',
793
+ appliesTo: C4_ROLE.person,
794
+ roles: C4_ROLES,
795
+ messageKey: 'com.labre.c4.validation.person-in-boundary',
796
+ messageFallback: 'This person is drawn inside a boundary.',
797
+ suggestionKey: 'com.labre.c4.validation.person-in-boundary.suggestion',
798
+ suggestionFallback: 'A boundary encloses the parts of one system; a person is never one of them — they are who uses it. Move them outside, and let the relationship cross the dashed line.',
799
+ version: 1,
800
+ provenance: {
801
+ source: 'labre-convention',
802
+ reference: 'Labre convention — C4 draws no line forbidding it; a boundary encloses parts of a system and a person is not one',
803
+ },
804
+ backgroundRole: C4_ROLE.boundary,
805
+ // The frame's own declaration, carried as data exactly like `roles` is: it is
806
+ // where the plot and its one zone are written, and the engine resolves the
807
+ // rectangle from it rather than knowing anything about C4.
808
+ background: C4_BOUNDARY_BACKGROUND,
809
+ inZone: {
810
+ // The boundary's single full-plot zone — see the header. It exists to carry
811
+ // the boundary's name and paints nothing, so it IS the inside of the frame.
812
+ zoneIds: ['name'],
813
+ expect: 'outside',
814
+ },
815
+ };
816
+ /* ── Zoom: is what is inside the frame at the frame's own level? ───────── */
817
+ /**
818
+ * The citation the three zoom rules share.
819
+ *
820
+ * One reference for one statement of C4's, read three ways — the same call
821
+ * {@link isolationRule} makes for the three isolation rules. Naming the
822
+ * abstractions rather than the review checklist is the honest half: the
823
+ * checklist never asks this question, and the model answers it before the
824
+ * checklist begins.
825
+ */
826
+ const ZOOM_PROVENANCE = {
827
+ source: 'recommendation',
828
+ reference: 'C4 model — the core abstractions (c4model.com/abstractions): a software system is made up of containers, each of which contains components; the levels are zooms of one element',
829
+ };
830
+ /**
831
+ * **C12** — a software system drawn inside a boundary.
832
+ *
833
+ * The first of the three zoom rules, and the widest: it is framed on the PARENT
834
+ * role, so ANY boundary counts — a system boundary, a container boundary, and a
835
+ * boundary drawn before either existed.
836
+ *
837
+ * ## What is wrong with the drawing
838
+ *
839
+ * A boundary already IS a system or a container: that is the whole of what the
840
+ * dashed rectangle says. So a software system drawn inside one is a claim the
841
+ * model has no room for — a system inside a system, or a system inside a
842
+ * container — and it is always one of two mistakes. Either the box is a
843
+ * CONTAINER the author drew with the system tool (the commonest, because the two
844
+ * are the same rounded rectangle in a different blue), or the zoom never
845
+ * happened: the author drew the frame for the next level down and then filled it
846
+ * with the level they were already on.
847
+ *
848
+ * Neither reading is a diagram the reader can follow, and no third reading makes
849
+ * it one — which is why the rule can be framed on the parent role without losing
850
+ * anything. Whichever level the boundary is at, a system does not go in it.
851
+ *
852
+ * ## Mechanics: `element-in-zone`, `expect: 'outside'`
853
+ *
854
+ * {@link personInBoundary}'s, exactly, and for its reasons:
855
+ * `element-in-background` has one polarity and no `expect` to turn round, while
856
+ * `element-in-zone` has the polarity and the boundary declares a single full-plot
857
+ * zone (`background.ts`) that IS the inside of the frame. The geometry comes from
858
+ * the declaration the renderer paints from, so a boundary that is moved or
859
+ * resized moves this rule with it.
860
+ *
861
+ * And the same two silences come with it: a system STRADDLING the dashed edge
862
+ * raises nothing (`element-in-zone` judges a subject only against the frame that
863
+ * CONTAINS it, and half in is not in), and a board with no boundary is silence
864
+ * twice over. Both are the drawing hand being left alone.
865
+ */
866
+ const systemInBoundary = {
867
+ id: 'c4.system-in-boundary',
868
+ framework: 'c4',
869
+ family: 'element-in-zone',
870
+ severity: 'audit',
871
+ appliesTo: C4_ROLE.system,
872
+ roles: C4_ROLES,
873
+ messageKey: 'com.labre.c4.validation.system-in-boundary',
874
+ messageFallback: 'This software system is drawn inside a boundary.',
875
+ suggestionKey: 'com.labre.c4.validation.system-in-boundary.suggestion',
876
+ suggestionFallback: 'A boundary already is a system or a container, so nothing inside it can be a system. If this box is one of the parts, draw it as a container; if it is the thing the boundary is about, move it out and let the frame carry its name.',
877
+ version: 1,
878
+ provenance: ZOOM_PROVENANCE,
879
+ // The PARENT role: any boundary, at any level, including one drawn before the
880
+ // level was written down. A system belongs inside none of them.
881
+ backgroundRole: C4_ROLE.boundary,
882
+ background: C4_BOUNDARY_BACKGROUND,
883
+ inZone: {
884
+ zoneIds: ['name'],
885
+ expect: 'outside',
886
+ },
887
+ };
888
+ /**
889
+ * **C13** — a container drawn inside a container boundary. The PO's zoom
890
+ * paradox, and the rule this whole slice exists for.
891
+ *
892
+ * A container boundary is the frame of a COMPONENT diagram: it says "this is one
893
+ * container, and here are its parts". A container drawn inside it is therefore
894
+ * either that container drawn inside itself, or a second container on a sheet
895
+ * that has zoomed past the level where containers live. Only components belong
896
+ * in there.
897
+ *
898
+ * ## The frame is the CHILD role, and that is the whole rule
899
+ *
900
+ * `c4:container-boundary`, never the parent. A container inside a SYSTEM boundary
901
+ * is the single most ordinary thing in C4 — it is what a container diagram IS —
902
+ * so a rule framed one role up would indict every correct container diagram ever
903
+ * drawn. The split exists to let this rule name the one frame it is about.
904
+ *
905
+ * A pre-split boundary carries the parent and matches no child, so this rule
906
+ * finds no frame on an old document and says nothing there. That is not a hole
907
+ * being papered over: such a boundary never said which level it was at, and
908
+ * guessing would mean indicting containers on the strength of a variant field
909
+ * this rule cannot see.
910
+ *
911
+ * ## The DATABASE comes free
912
+ *
913
+ * `appliesTo: c4:container` reaches `c4:database` through `roleIsA`, and reaches
914
+ * the mobile app and the single-page app without even that — they carry
915
+ * `c4:container` outright (`roles.ts`). A data store drawn inside a container
916
+ * boundary is the same paradox with a cylinder.
917
+ */
918
+ const containerInContainerBoundary = {
919
+ id: 'c4.container-in-container-boundary',
920
+ framework: 'c4',
921
+ family: 'element-in-zone',
922
+ severity: 'audit',
923
+ appliesTo: C4_ROLE.container,
924
+ roles: C4_ROLES,
925
+ messageKey: 'com.labre.c4.validation.container-in-container-boundary',
926
+ messageFallback: 'This container is drawn inside a container boundary.',
927
+ suggestionKey: 'com.labre.c4.validation.container-in-container-boundary.suggestion',
928
+ suggestionFallback: 'A container boundary IS that container, and what goes inside it is its components. Draw this box as a component — or, if the sheet is really about the containers of a system, make the frame a system boundary.',
929
+ version: 1,
930
+ provenance: ZOOM_PROVENANCE,
931
+ // The CHILD role, and it has to be: a container inside a SYSTEM boundary is
932
+ // what a container diagram is made of.
933
+ backgroundRole: C4_ROLE['container-boundary'],
934
+ background: C4_BOUNDARY_BACKGROUND,
935
+ inZone: {
936
+ zoneIds: ['name'],
937
+ expect: 'outside',
938
+ },
939
+ };
940
+ /**
941
+ * **C14** — a component framed by a system boundary and by no container.
942
+ *
943
+ * The sharp half of {@link homelessComponent} — read that rule's comment first,
944
+ * it carries the argument for why this is a second rule and not a retarget of the
945
+ * first — and the third zoom rule.
946
+ *
947
+ * A component is a part of a CONTAINER. The frame that says which container is a
948
+ * container boundary, so a component framed only by a system boundary has skipped
949
+ * the level the model puts between them: the sheet is a container diagram with
950
+ * components drawn on it, and the reader cannot say which container any of them
951
+ * belongs to. The zoom went from system straight to component.
952
+ *
953
+ * ## Why `element-in-background` and not the zone family the other two use
954
+ *
955
+ * Because the honest question is a POSITIVE one — "is a container claiming this
956
+ * component?" — and the negative reading is not equivalent on this canvas. A
957
+ * container boundary is normally drawn INSIDE a system boundary, so "the
958
+ * component is inside a system boundary" is true of every conformant component
959
+ * diagram that also draws the system frame, and `element-in-zone` judges a
960
+ * subject against whichever frame contains it with no notion of a nearer one.
961
+ * The rule would fire on the correct drawing.
962
+ *
963
+ * `element-in-background` asks the positive question directly: a component not
964
+ * contained by ANY `c4:container-boundary` is in violation, and a component
965
+ * nested in one is silent however many other frames are drawn round it.
966
+ *
967
+ * ## What it stays silent about, which is exactly the compatibility promise
968
+ *
969
+ * A board with no container boundary at all — including every board drawn before
970
+ * the role split, whose boundaries all carry the parent role — has no frame of
971
+ * this rule's for anything to be inside or outside of, and
972
+ * `element-in-background` answers that with silence. An old document therefore
973
+ * gains NOTHING from this rule, and {@link homelessComponent} keeps saying on it
974
+ * precisely what it said before.
975
+ *
976
+ * The same silence covers the honest sketch: a component diagram whose author has
977
+ * not drawn the container frame yet is a sketch, which is the reading
978
+ * {@link homelessComponent} already documents and the one that keeps this from
979
+ * being the rule that lights up a board in its first minute.
980
+ *
981
+ * ## The KNOWN LIMIT, which is the other face of that silence
982
+ *
983
+ * A sheet with a system boundary, components drawn inside it and NO container
984
+ * boundary anywhere raises nothing — and that is the level skip that is easiest
985
+ * to draw. The rule cannot see it, because the only frame it could read is the
986
+ * one nobody drew.
987
+ *
988
+ * Closing it here would mean asking the opposite question — "is this component
989
+ * inside a SYSTEM boundary?" — and that question indicts the CONFORMANT drawing:
990
+ * a container boundary is normally drawn inside a system boundary, so a
991
+ * component correctly nested in the container frame is inside the system frame
992
+ * too, and `element-in-zone` judges it against whichever frame contains it with
993
+ * no notion of a nearer one. A rule that fires on the right diagram is worse than
994
+ * one that misses a wrong one.
995
+ *
996
+ * What CAN see it is a BOARD that declares which level it is drawing, and that
997
+ * now exists: {@link containerDiagramLevel} indicts the components on a sheet
998
+ * whose author has said it is a container diagram, reading the level off the
999
+ * sheet rather than guessing it from what happens to be drawn on it. This rule
1000
+ * is unchanged and still says nothing there — the two compose, the board-level
1001
+ * one covering exactly the case this one structurally cannot.
1002
+ *
1003
+ * The limit therefore survives in one shape only, and it is the honest one: a
1004
+ * board that declares NO level. Nothing on such a sheet says which diagram it
1005
+ * is, so nothing can say the level has been skipped. That is a sketch, and a
1006
+ * sketch is left alone.
1007
+ */
1008
+ const componentLevelSkip = {
1009
+ id: 'c4.component-level-skip',
1010
+ framework: 'c4',
1011
+ family: 'element-in-background',
1012
+ severity: 'audit',
1013
+ appliesTo: C4_ROLE.component,
1014
+ roles: C4_ROLES,
1015
+ messageKey: 'com.labre.c4.validation.component-level-skip',
1016
+ messageFallback: 'No container boundary claims this component.',
1017
+ suggestionKey: 'com.labre.c4.validation.component-level-skip.suggestion',
1018
+ suggestionFallback: 'A component is a part of one container, and a system boundary is not a container: as drawn, the sheet jumps from the system to its components and the reader cannot tell which container this is in. Draw the container boundary round the parts that belong to it.',
1019
+ version: 1,
1020
+ provenance: ZOOM_PROVENANCE,
1021
+ // The CHILD role: the frame that names a container, and the only one that can
1022
+ // answer this question. No `background` declaration — `element-in-background`
1023
+ // measures against the element BOX and reads no margin.
1024
+ backgroundRole: C4_ROLE['container-boundary'],
1025
+ };
1026
+ /* ── Level: is what is on the SHEET at the level the sheet declares? ────── */
1027
+ /**
1028
+ * The citation the two level rules share.
1029
+ *
1030
+ * The DIAGRAMS pages rather than the abstractions page the zoom rules cite:
1031
+ * what those three rules read is what the model IS ("a system is made of
1032
+ * containers"), while these two read what each SHEET shows — which C4 states
1033
+ * separately, one page per level, as the definition of the diagram type.
1034
+ */
1035
+ const LEVEL_PROVENANCE = {
1036
+ source: 'recommendation',
1037
+ reference: 'C4 model — the diagram types (c4model.com): the system context, container and component diagrams each define what is drawn on them',
1038
+ };
1039
+ /**
1040
+ * **C15** — a container, a component or a boundary on a board that says it is a
1041
+ * CONTEXT diagram.
1042
+ *
1043
+ * The first rule in this pack — in this library — whose subject is the SHEET.
1044
+ * Everything before it reads an artefact and looks around it; this one reads a
1045
+ * fact the board itself declares and judges what has been drawn on it.
1046
+ *
1047
+ * ## What the board now says, and why it had to say it
1048
+ *
1049
+ * `c4.component-level-skip` documents the hole this closes: a sheet with a
1050
+ * system boundary, components inside it and NO container boundary anywhere
1051
+ * raises nothing, because the only frame that rule could read is the one nobody
1052
+ * drew. No rule reading the drawing alone can see that — a component correctly
1053
+ * nested in a container boundary is inside the system boundary too, so the
1054
+ * negative reading indicts the conformant diagram. The level had to come from
1055
+ * somewhere the drawing does not, and the only honest source is the author.
1056
+ *
1057
+ * So the board carries an optional `level` (`C4BoardElementModel`), the picker
1058
+ * writes it, and a board that never states one is a free sketch judged by
1059
+ * nothing here — which is every C4 diagram drawn before this slice.
1060
+ *
1061
+ * ## What a context diagram forbids, and the much longer list it does not
1062
+ *
1063
+ * Three things: containers (which reaches the database, the mobile app and the
1064
+ * single-page app through `roleIsA`), components, and BOUNDARIES at either
1065
+ * level, including one drawn before the split.
1066
+ *
1067
+ * The boundary is the entry a reader will question, so it is the one worth
1068
+ * spelling out: a context diagram draws the system it is about as a BOX, with
1069
+ * the people and the neighbouring systems around it. The dashed frame is how
1070
+ * C4 says "here is the inside of that box", which is precisely the move a
1071
+ * context diagram has not made yet — an enterprise-boundary variant exists in
1072
+ * the wider C4 literature, and this editor draws neither of its two frames as
1073
+ * one, so a boundary here is a zoom on a sheet that declared it was not zoomed.
1074
+ *
1075
+ * Everything else is legal and deliberately so. Persons, systems, relationships,
1076
+ * titles, type lines, descriptions, legend glyphs, sticky notes and the free
1077
+ * rectangles somebody thought with are all left alone: C4 context diagrams are
1078
+ * made of neighbours, and a deny-list that named what is admitted rather than
1079
+ * what is refused would indict half of them.
1080
+ */
1081
+ const contextDiagramLevel = {
1082
+ id: 'c4.context-diagram-level',
1083
+ framework: 'c4',
1084
+ family: 'view-admissibility',
1085
+ severity: 'audit',
1086
+ // No `appliesTo`: the subjects are declared per LEVEL, in `admissibility`,
1087
+ // and naming a single one here would be data that lies.
1088
+ roles: C4_ROLES,
1089
+ messageKey: 'com.labre.c4.validation.context-diagram-level',
1090
+ messageFallback: 'This board is a context diagram, and containers, components and boundaries are not drawn on one.',
1091
+ suggestionKey: 'com.labre.c4.validation.context-diagram-level.suggestion',
1092
+ suggestionFallback: 'A context diagram shows the system in the middle, the people who use it and the systems around it — every one of them a box, none of them a frame. Draw the parts on a container diagram, or set this board to the level it really shows.',
1093
+ version: 1,
1094
+ provenance: LEVEL_PROVENANCE,
1095
+ backgroundRole: C4_ROLE.board,
1096
+ admissibility: {
1097
+ // The prop the board writes its level in. The rule names it; the engine
1098
+ // reads it; nothing in between knows the word "C4".
1099
+ levelProp: 'level',
1100
+ forbidden: {
1101
+ context: [
1102
+ // Reaches `c4:database` by declaration, and the mobile app and the
1103
+ // single-page app outright — they carry `c4:container` (`roles.ts`).
1104
+ C4_ROLE.container,
1105
+ C4_ROLE.component,
1106
+ // The PARENT role: both children, and every boundary drawn before the
1107
+ // split. A context diagram has not zoomed into anything yet.
1108
+ C4_ROLE.boundary,
1109
+ ],
1110
+ },
1111
+ },
1112
+ };
1113
+ /**
1114
+ * **C16** — a component or a container boundary on a board that says it is a
1115
+ * CONTAINER diagram.
1116
+ *
1117
+ * The sharp end of the slice, and the rule that finally sees
1118
+ * `c4.component-level-skip`'s known limit: a system boundary full of components
1119
+ * with no container boundary anywhere is a container diagram (or a context one)
1120
+ * with the container level missing, and now that the board says which sheet it
1121
+ * is, the components can be named for what they are.
1122
+ *
1123
+ * ## What a container diagram forbids, which is two things and not four
1124
+ *
1125
+ * Components, and CONTAINER boundaries. Everything else a container diagram
1126
+ * legitimately shows: the persons who use the system, the neighbouring systems
1127
+ * it talks to, the containers themselves, and the SYSTEM boundary drawn round
1128
+ * them — which is the frame a container diagram is defined by, so forbidding it
1129
+ * would indict the textbook drawing.
1130
+ *
1131
+ * The container boundary is out for the reason `c4.container-in-container-boundary`
1132
+ * gives from the other side: that frame IS a container, and what goes inside it
1133
+ * is components — which is a component diagram, on a sheet that says it is a
1134
+ * container one.
1135
+ *
1136
+ * ## The interplay with the zoom rules, stated rather than engineered away
1137
+ *
1138
+ * A component drawn inside a proper container boundary on a `container`-level
1139
+ * board raises BOTH this rule and, from the frame's side, nothing else — the
1140
+ * component is claimed by a container boundary, so `c4.component-level-skip` is
1141
+ * silent, and `c4.container-in-container-boundary` is about containers. The
1142
+ * board-level rule fires alone, and it is right to: a container diagram showing
1143
+ * a container boundary with components inside it is a COMPONENT diagram, and
1144
+ * the author fixes it by saying so — set the level to Component — or by taking
1145
+ * the zoom off this sheet and drawing it on its own.
1146
+ *
1147
+ * That is a deliberate v1 stance, and the alternative was considered and
1148
+ * rejected: exempting elements inside a boundary the zoom rules already govern
1149
+ * would mean the board's own declaration stops applying wherever somebody drew
1150
+ * a frame, which is the level saying one thing and the sheet showing another
1151
+ * with nothing to report. The board's level judges the whole board. A sheet
1152
+ * that wants two levels is two sheets.
1153
+ *
1154
+ * (An element's effective level could one day be DERIVED from the innermost
1155
+ * boundary containing it rather than from the board — a derivation family, not
1156
+ * a rule. Nothing here builds it, and the zoom rules from the boundary slice
1157
+ * already police what is inside a frame; this is the note that says the two
1158
+ * readings exist and that v1 chose the simpler one.)
1159
+ */
1160
+ const containerDiagramLevel = {
1161
+ id: 'c4.container-diagram-level',
1162
+ framework: 'c4',
1163
+ family: 'view-admissibility',
1164
+ severity: 'audit',
1165
+ roles: C4_ROLES,
1166
+ messageKey: 'com.labre.c4.validation.container-diagram-level',
1167
+ messageFallback: 'This board is a container diagram, and components and container boundaries are not drawn on one.',
1168
+ suggestionKey: 'com.labre.c4.validation.container-diagram-level.suggestion',
1169
+ suggestionFallback: 'A container diagram shows one system’s containers — with the people and the neighbouring systems around them, and a system boundary round the containers themselves. A container zoomed open is the next sheet down: set this board to Component, or move the zoom to its own board.',
1170
+ version: 1,
1171
+ provenance: LEVEL_PROVENANCE,
1172
+ backgroundRole: C4_ROLE.board,
1173
+ admissibility: {
1174
+ levelProp: 'level',
1175
+ forbidden: {
1176
+ container: [
1177
+ C4_ROLE.component,
1178
+ // The CHILD role, never the parent: the system boundary is the frame a
1179
+ // container diagram is DEFINED by, and a boundary drawn before the
1180
+ // split never said which level it was at.
1181
+ C4_ROLE['container-boundary'],
1182
+ ],
1183
+ },
1184
+ },
1185
+ };
1186
+ /**
1187
+ * ## And the two levels that declare no rule at all
1188
+ *
1189
+ * C4 is named after its four C's — Context, Containers, Components, Code — and
1190
+ * the picker offers all four (`levels.ts`). Two of them are levels a board can
1191
+ * carry and NOTHING in this pack judges, deliberately, and it is worth saying
1192
+ * why rather than leaving a reader to notice the gap.
1193
+ *
1194
+ * **`component`** — a component diagram shows one container's components, and
1195
+ * around them it legitimately shows the containers they talk to, the systems
1196
+ * behind those, and the people at the top of the chain: C4 draws the neighbours
1197
+ * at every level. The frame it is defined by is a container boundary, which is
1198
+ * therefore legal too, and a system boundary drawn round that is the ordinary
1199
+ * nesting. That leaves no role a component diagram refuses.
1200
+ *
1201
+ * **`code`** — for the opposite reason. Not "nothing is forbidden" but "we know
1202
+ * nothing yet": the editor draws no code-level artefact, so the pack has no
1203
+ * vocabulary in which to say what such a sheet admits or refuses. The
1204
+ * declaration is still the author's to make — that is a decision about the
1205
+ * NOTATION's vocabulary, not about our tooling (`C4BoardLevel` says so at
1206
+ * length) — and the tool records it without pretending to check it.
1207
+ *
1208
+ * A rule declared for either would be an empty `forbidden` list — data that can
1209
+ * never fire, which this file already calls the worst thing declarative data can
1210
+ * do. So there are two rules and not four, and {@link ViewAdmissibilityDef} is
1211
+ * built to make that the natural outcome: a level absent from the table is a
1212
+ * level the rule has nothing to say about, and the engine walks nothing for it.
1213
+ */
1214
+ /**
1215
+ * The pack, whole: sixteen rules, all registered, all live.
1216
+ *
1217
+ * FIVE families, where fourteen rules needed four. C4 has one connecting object,
1218
+ * four flat levels and two frames, so there is still no swimlane question, no
1219
+ * graph traversal and no cardinality per frame to ask about — and the three zoom
1220
+ * rules added nothing to the list, being two more `element-in-zone` rules and one
1221
+ * more `element-in-background` rule, because the split that made them askable
1222
+ * happened in the role VOCABULARY and not in the engine.
1223
+ *
1224
+ * The two LEVEL rules are the exception, and the fifth family is the honest
1225
+ * reason: their question is asked of the SHEET, off a fact the sheet declares,
1226
+ * and no family that starts from an artefact can express it. That is also why
1227
+ * `view-admissibility` is generic and lives in the engine — C4 is its first
1228
+ * consumer, not its owner.
1229
+ *
1230
+ * Sixteen and not fifteen because the grammar and the self-loop are two rules
1231
+ * (see {@link relationshipSelfLoop}); sixteen and not nineteen because the four
1232
+ * per-level naming rules collapsed into {@link unnamedElement} the moment an
1233
+ * element's name became one text role instead of four shapes' inner text; and
1234
+ * sixteen and not eighteen because two of the four levels a board can declare
1235
+ * are judged by nothing — `component` forbids nothing and `code` is a level this
1236
+ * pack cannot yet speak about (see above).
1237
+ */
1238
+ export const C4_RULES = [
1239
+ // Naming: does the drawing say anything at all?
1240
+ unlabeledRelationship,
1241
+ unnamedElement,
1242
+ // Grammar: what an arrow may run between, and whether it is an arrow at all.
1243
+ untypedLink,
1244
+ relationshipEndpoints,
1245
+ relationshipSelfLoop,
1246
+ // Degree: does the element earn its place on the sheet?
1247
+ isolatedSystem,
1248
+ isolatedContainer,
1249
+ isolatedComponent,
1250
+ databaseInitiates,
1251
+ // Membership: which frame does the element belong to?
1252
+ homelessComponent,
1253
+ personInBoundary,
1254
+ // Zoom: is what is inside the frame at the frame's own level?
1255
+ systemInBoundary,
1256
+ containerInContainerBoundary,
1257
+ componentLevelSkip,
1258
+ // Level: is what is on the SHEET at the level the sheet declares?
1259
+ contextDiagramLevel,
1260
+ containerDiagramLevel,
1261
+ ];