@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.
- package/dist/actions.d.ts +179 -0
- package/dist/actions.js +375 -0
- package/dist/background.d.ts +77 -0
- package/dist/background.js +223 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +221 -0
- package/dist/component.d.ts +192 -0
- package/dist/component.js +188 -0
- package/dist/consts.d.ts +331 -0
- package/dist/consts.js +384 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +18 -0
- package/dist/element-renderer.js +14 -0
- package/dist/element-view.d.ts +51 -0
- package/dist/element-view.js +146 -0
- package/dist/export.d.ts +184 -0
- package/dist/export.js +454 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +52 -0
- package/dist/interchange.d.ts +74 -0
- package/dist/interchange.js +143 -0
- package/dist/legend.d.ts +37 -0
- package/dist/legend.js +123 -0
- package/dist/levels.d.ts +70 -0
- package/dist/levels.js +46 -0
- package/dist/morph.d.ts +89 -0
- package/dist/morph.js +229 -0
- package/dist/node/node-renderer.d.ts +6 -0
- package/dist/node/node-renderer.js +304 -0
- package/dist/node/node-view.d.ts +45 -0
- package/dist/node/node-view.js +80 -0
- package/dist/node/type-line-watcher.d.ts +70 -0
- package/dist/node/type-line-watcher.js +142 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.js +149 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +177 -0
- package/dist/roles.d.ts +116 -0
- package/dist/roles.js +303 -0
- package/dist/rules.d.ts +95 -0
- package/dist/rules.js +1261 -0
- package/dist/toolbar/c4-menu.d.ts +11 -0
- package/dist/toolbar/c4-menu.js +14 -0
- package/dist/toolbar/c4-senior-button.d.ts +19 -0
- package/dist/toolbar/c4-senior-button.js +23 -0
- package/dist/toolbar/config.d.ts +150 -0
- package/dist/toolbar/config.js +436 -0
- package/dist/toolbar/icons.d.ts +90 -0
- package/dist/toolbar/icons.js +157 -0
- package/dist/toolbar/senior-tool.d.ts +1 -0
- package/dist/toolbar/senior-tool.js +11 -0
- package/dist/translations.d.ts +18 -0
- package/dist/translations.js +42 -0
- package/dist/type-line.d.ts +175 -0
- package/dist/type-line.js +244 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +148 -0
- 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
|
+
];
|