@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
@@ -0,0 +1,70 @@
1
+ import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
2
+ /**
3
+ * Keeps a component's type line SEMI-DERIVED: the author owns the technology,
4
+ * the notation owns the word.
5
+ *
6
+ * ## The rule, and where it is applied
7
+ *
8
+ * `[Container: Java]` is two statements in one line. Which of the four levels
9
+ * this box is, is the diagram's — it comes from `kind`, it is what the renderer
10
+ * paints and what the exporter maps, and an author retyping it would let the
11
+ * words disagree with the picture. What it is built with is theirs, and it is
12
+ * the only half the notation leaves to them (PO arbitration, 28/08/2026).
13
+ *
14
+ * Since the tier is a real canvas text element that half is typed IN PLACE, on
15
+ * the picture, with nothing standing between the keyboard and the document. So
16
+ * this watcher reads back what was typed, keeps the technology, and rewrites the
17
+ * canonical line: `Java` becomes `[Container: Java]`, `[Person: Java]` on a
18
+ * container becomes `[Container: Java]`, an emptied line becomes `[Container]`.
19
+ *
20
+ * ## Why the seam is the SELECTION and not `elementUpdated`
21
+ *
22
+ * The obvious hook is `surface.elementUpdated` filtered on `props.text`, which
23
+ * is how every other reaction in this library is wired. It is the wrong one
24
+ * here, and visibly so: a canvas text editor binds its inline editor straight
25
+ * onto the element's `Y.Text`, so that signal fires once per KEYSTROKE. A
26
+ * normalizer on it would rewrite the line under the cursor while somebody was
27
+ * still typing into it — an author who has got as far as `[Container: Ja` would
28
+ * have their caret thrown by a rewrite of the very text they are editing.
29
+ *
30
+ * "The edit committed" is therefore read where the editor actually says so: the
31
+ * text editor holds an EDITING selection for as long as it is mounted and drops
32
+ * it when it closes, so an id leaving the editing set is a commit and nothing
33
+ * else is. One rewrite per edit, after the editor is gone, and a single undo
34
+ * entry for it.
35
+ *
36
+ * ## Always-on, and local-only
37
+ *
38
+ * Registered by the RENDER half, not the flag-gated one (`docs/adr/0009`): this
39
+ * does not author anything: it keeps an element that is ALREADY in the document
40
+ * readable while somebody edits it, and a diagram drawn while the C4 button was
41
+ * on must stay editable when it goes off. It is also inherently local — a
42
+ * selection is this peer's own, so there is no fleet of observers to filter out
43
+ * the way `EdgyRelationResolver` has to.
44
+ */
45
+ export declare class C4TypeLineWatcher extends InteractivityExtension {
46
+ static key: string;
47
+ private _subscription;
48
+ private _editing;
49
+ mounted(): void;
50
+ unmounted(): void;
51
+ /** The kind of the C4 shape this element is grouped with, if there is one. */
52
+ private _kindOf;
53
+ private _normalize;
54
+ }
55
+ /**
56
+ * Which ids just stopped being edited, and what is being edited now.
57
+ *
58
+ * Split out as a pure function because it is the whole of the seam's logic and
59
+ * the only part of it worth being wrong about: an id that appears in the new
60
+ * editing set has merely started, an id in both is still going, and only one
61
+ * that has LEFT is a commit. Kept total over an empty selection, which is what
62
+ * arrives when the editor closes onto bare canvas.
63
+ */
64
+ export declare function c4EditingTransition(previous: ReadonlySet<string>, selections: readonly {
65
+ elements: readonly string[];
66
+ editing?: boolean;
67
+ }[]): {
68
+ editing: Set<string>;
69
+ left: string[];
70
+ };
@@ -0,0 +1,142 @@
1
+ import { C4NodeElementModel, GroupElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
2
+ import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
3
+ import { c4ComponentSiblings } from '../component.js';
4
+ import { C4_ROLE } from '../roles.js';
5
+ import { normalizeC4TypeLine } from '../type-line.js';
6
+ /**
7
+ * Keeps a component's type line SEMI-DERIVED: the author owns the technology,
8
+ * the notation owns the word.
9
+ *
10
+ * ## The rule, and where it is applied
11
+ *
12
+ * `[Container: Java]` is two statements in one line. Which of the four levels
13
+ * this box is, is the diagram's — it comes from `kind`, it is what the renderer
14
+ * paints and what the exporter maps, and an author retyping it would let the
15
+ * words disagree with the picture. What it is built with is theirs, and it is
16
+ * the only half the notation leaves to them (PO arbitration, 28/08/2026).
17
+ *
18
+ * Since the tier is a real canvas text element that half is typed IN PLACE, on
19
+ * the picture, with nothing standing between the keyboard and the document. So
20
+ * this watcher reads back what was typed, keeps the technology, and rewrites the
21
+ * canonical line: `Java` becomes `[Container: Java]`, `[Person: Java]` on a
22
+ * container becomes `[Container: Java]`, an emptied line becomes `[Container]`.
23
+ *
24
+ * ## Why the seam is the SELECTION and not `elementUpdated`
25
+ *
26
+ * The obvious hook is `surface.elementUpdated` filtered on `props.text`, which
27
+ * is how every other reaction in this library is wired. It is the wrong one
28
+ * here, and visibly so: a canvas text editor binds its inline editor straight
29
+ * onto the element's `Y.Text`, so that signal fires once per KEYSTROKE. A
30
+ * normalizer on it would rewrite the line under the cursor while somebody was
31
+ * still typing into it — an author who has got as far as `[Container: Ja` would
32
+ * have their caret thrown by a rewrite of the very text they are editing.
33
+ *
34
+ * "The edit committed" is therefore read where the editor actually says so: the
35
+ * text editor holds an EDITING selection for as long as it is mounted and drops
36
+ * it when it closes, so an id leaving the editing set is a commit and nothing
37
+ * else is. One rewrite per edit, after the editor is gone, and a single undo
38
+ * entry for it.
39
+ *
40
+ * ## Always-on, and local-only
41
+ *
42
+ * Registered by the RENDER half, not the flag-gated one (`docs/adr/0009`): this
43
+ * does not author anything: it keeps an element that is ALREADY in the document
44
+ * readable while somebody edits it, and a diagram drawn while the C4 button was
45
+ * on must stay editable when it goes off. It is also inherently local — a
46
+ * selection is this peer's own, so there is no fleet of observers to filter out
47
+ * the way `EdgyRelationResolver` has to.
48
+ */
49
+ export class C4TypeLineWatcher extends InteractivityExtension {
50
+ constructor() {
51
+ super(...arguments);
52
+ this._subscription = null;
53
+ this._editing = new Set();
54
+ }
55
+ static { this.key = 'c4-type-line-watcher'; }
56
+ mounted() {
57
+ this._subscription = this.gfx.selection.slots.updated.subscribe(selections => {
58
+ const { editing, left } = c4EditingTransition(this._editing, selections);
59
+ this._editing = editing;
60
+ for (const id of left)
61
+ this._normalize(id);
62
+ });
63
+ }
64
+ unmounted() {
65
+ this._subscription?.unsubscribe();
66
+ this._subscription = null;
67
+ this._editing = new Set();
68
+ super.unmounted();
69
+ }
70
+ /** The kind of the C4 shape this element is grouped with, if there is one. */
71
+ _kindOf(elementId) {
72
+ const surface = this.gfx.surface;
73
+ if (!surface)
74
+ return null;
75
+ const groups = surface.elementModels.filter((element) => element instanceof GroupElementModel);
76
+ for (const siblingId of c4ComponentSiblings(elementId, groups)) {
77
+ const sibling = surface.getElementById(siblingId);
78
+ if (sibling instanceof C4NodeElementModel)
79
+ return sibling.kind;
80
+ }
81
+ return null;
82
+ }
83
+ _normalize(elementId) {
84
+ // A read-only document is READ: every write below would be refused, and the
85
+ // author cannot have typed anything to normalize in the first place.
86
+ if (this.std.store.readonly)
87
+ return;
88
+ const surface = this.gfx.surface;
89
+ if (!surface)
90
+ return;
91
+ const element = surface.getElementById(elementId);
92
+ if (!(element instanceof TextElementModel))
93
+ return;
94
+ if (element.role !== C4_ROLE['type-line'])
95
+ return;
96
+ if (element.isLocked())
97
+ return;
98
+ const kind = this._kindOf(elementId);
99
+ // A type line whose shape is gone — the node deleted, the group released
100
+ // and the text dragged away — has no kind to derive a word from. Left
101
+ // exactly as the author typed it: guessing at a level would be this watcher
102
+ // asserting something nobody drew.
103
+ if (!kind)
104
+ return;
105
+ const raw = element.text.toString();
106
+ const canonical = normalizeC4TypeLine(kind, raw);
107
+ if (canonical === raw)
108
+ return;
109
+ this.std.store.captureSync();
110
+ // Mutated IN PLACE rather than replaced: the `Y.Text` instance is what the
111
+ // element's own change watcher and any bound view hold, and swapping it for
112
+ // a fresh one would leave both pointing at a text nobody is editing. One
113
+ // transaction, so the rewrite is one undo entry and not two.
114
+ this.std.store.transact(() => {
115
+ element.text.delete(0, element.text.length);
116
+ element.text.insert(0, canonical);
117
+ });
118
+ }
119
+ }
120
+ /**
121
+ * Which ids just stopped being edited, and what is being edited now.
122
+ *
123
+ * Split out as a pure function because it is the whole of the seam's logic and
124
+ * the only part of it worth being wrong about: an id that appears in the new
125
+ * editing set has merely started, an id in both is still going, and only one
126
+ * that has LEFT is a commit. Kept total over an empty selection, which is what
127
+ * arrives when the editor closes onto bare canvas.
128
+ */
129
+ export function c4EditingTransition(previous, selections) {
130
+ const editing = new Set();
131
+ for (const selection of selections) {
132
+ if (!selection.editing)
133
+ continue;
134
+ for (const id of selection.elements)
135
+ editing.add(id);
136
+ }
137
+ const left = [];
138
+ for (const id of previous)
139
+ if (!editing.has(id))
140
+ left.push(id);
141
+ return { editing, left };
142
+ }
@@ -0,0 +1,84 @@
1
+ import { type C4NodeKind } from '@formicoidea/labre-core/model';
2
+ /**
3
+ * What a C4 shape IS, as props — the ONE description the creation site and the
4
+ * morph both read.
5
+ *
6
+ * Split out of `actions.ts` when the morph landed, for the reason BPMN's
7
+ * `presets.ts` gives about `bpmnNodeProps`: a kind's appearance is written by
8
+ * the preset of the kind it was created as and nothing else ever rewrites it,
9
+ * so a morph that restated the table would agree with the palette the day it
10
+ * was written and drift on the first restyle. Derived, they cannot: a morphed
11
+ * component and one freshly drawn from the sub-menu are the same element.
12
+ */
13
+ /**
14
+ * The kinds whose GLYPH draws the body, so the native shape underneath paints
15
+ * nothing at all.
16
+ *
17
+ * Five of nine, and the renderer is the authority on which. A person is a head
18
+ * fused into a rounded body and a database is a cylinder, neither of which a
19
+ * native rect can be. `mobile` and `browser` joined them with the PO's recette
20
+ * of 27/08/2026: the reference stencil paints their OUTER rectangle in the
21
+ * node's darker colour — the bezel — and insets a lighter SCREEN in it, which is
22
+ * the reverse of a band painted over a body, so there is nothing left for a
23
+ * native rect to contribute. Same call BPMN makes for `dataObject` /
24
+ * `dataStore`.
25
+ *
26
+ * They stay hit-testable across their whole area all the same:
27
+ * `C4NodeElementModel.includesPoint` forces the interior test regardless of
28
+ * `filled` — which is the fix for the PO's second report, that these nodes could
29
+ * not be double-clicked into their text editor.
30
+ *
31
+ * This is also the one table the container FAMILY morphs across: a `container`
32
+ * is a plain filled rect and a `database`, a `mobile` and a `browser` are not,
33
+ * so `filled` and `strokeStyle` genuinely flip on the way between them — which
34
+ * is exactly why {@link c4MorphProps} is the whole preset and not `{kind, role}`.
35
+ */
36
+ export declare const GLYPH_BODY_KINDS: ReadonlySet<C4NodeKind>;
37
+ /**
38
+ * Every prop a C4 shape is created with, for one kind and one box.
39
+ *
40
+ * `index` is deliberately NOT here: it is a layering statement the creation
41
+ * site makes about the five elements it is about to write in painting order,
42
+ * and it means nothing to an element that already exists.
43
+ *
44
+ * No `text` either, and that is the arrangement rather than an omission: since
45
+ * the PO's recette of 28/08/2026 the name is a `c4:title` CHILD, and the shape
46
+ * is a body and nothing else. Its text COLOUR is still seeded — an element
47
+ * drawn before that change carries its name here, and the node view routes a
48
+ * double-click to that editor for exactly those, so the words have to stay
49
+ * legible.
50
+ */
51
+ export declare function c4NodeProps(kind: C4NodeKind, box: {
52
+ xywh: string;
53
+ }): Record<string, unknown> & {
54
+ type: string;
55
+ };
56
+ /**
57
+ * What a kind is worth to a shape that ALREADY EXISTS — {@link c4NodeProps}
58
+ * minus the three things a morph has no business touching.
59
+ *
60
+ * ## Why the whole preset, and not `{kind, role}`
61
+ *
62
+ * Because on this pack's own table it is visibly not enough. The container
63
+ * family — `container`, `database`, `mobile`, `browser` — is four kinds where
64
+ * one paints its body natively and three hand it to the renderer, so `filled`
65
+ * and `strokeStyle` flip between them, and two of them round their corners
66
+ * where the others do not ({@link NODE_RADIUS}). Morph a container to a
67
+ * database with two keys and the cylinder arrives with the rectangle still
68
+ * painted behind it. The `-ext` families are the same argument in colour: grey
69
+ * IS what "somebody else owns this" means, and it lives in `fillColor` /
70
+ * `strokeColor`.
71
+ */
72
+ export declare function c4MorphProps(kind: C4NodeKind): Record<string, unknown>;
73
+ /**
74
+ * The fields to DELETE after morphing to `kind` — the keys some other kind
75
+ * writes and this one does not.
76
+ *
77
+ * EMPTY for every kind today, because no C4 preset spreads anything
78
+ * conditionally: all nine write the same key set with different values. Kept
79
+ * anyway, and derived rather than hard-coded to `[]`, for the reason BPMN's
80
+ * equivalent exists at all — a patch cannot express absence, and the day one
81
+ * kind stops writing a key the previous kind's value would otherwise stay in
82
+ * the Y.Map, silently in force.
83
+ */
84
+ export declare function c4MorphClears(kind: C4NodeKind): readonly string[];
@@ -0,0 +1,149 @@
1
+ import { FontFamily, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
2
+ import { NODE_PALETTE, NODE_RADIUS, NODE_STROKE_WIDTH, TITLE_FONT_SIZE, } from './consts.js';
3
+ import { C4_ROLE_OF_KIND } from './roles.js';
4
+ /**
5
+ * What a C4 shape IS, as props — the ONE description the creation site and the
6
+ * morph both read.
7
+ *
8
+ * Split out of `actions.ts` when the morph landed, for the reason BPMN's
9
+ * `presets.ts` gives about `bpmnNodeProps`: a kind's appearance is written by
10
+ * the preset of the kind it was created as and nothing else ever rewrites it,
11
+ * so a morph that restated the table would agree with the palette the day it
12
+ * was written and drift on the first restyle. Derived, they cannot: a morphed
13
+ * component and one freshly drawn from the sub-menu are the same element.
14
+ */
15
+ /**
16
+ * The kinds whose GLYPH draws the body, so the native shape underneath paints
17
+ * nothing at all.
18
+ *
19
+ * Five of nine, and the renderer is the authority on which. A person is a head
20
+ * fused into a rounded body and a database is a cylinder, neither of which a
21
+ * native rect can be. `mobile` and `browser` joined them with the PO's recette
22
+ * of 27/08/2026: the reference stencil paints their OUTER rectangle in the
23
+ * node's darker colour — the bezel — and insets a lighter SCREEN in it, which is
24
+ * the reverse of a band painted over a body, so there is nothing left for a
25
+ * native rect to contribute. Same call BPMN makes for `dataObject` /
26
+ * `dataStore`.
27
+ *
28
+ * They stay hit-testable across their whole area all the same:
29
+ * `C4NodeElementModel.includesPoint` forces the interior test regardless of
30
+ * `filled` — which is the fix for the PO's second report, that these nodes could
31
+ * not be double-clicked into their text editor.
32
+ *
33
+ * This is also the one table the container FAMILY morphs across: a `container`
34
+ * is a plain filled rect and a `database`, a `mobile` and a `browser` are not,
35
+ * so `filled` and `strokeStyle` genuinely flip on the way between them — which
36
+ * is exactly why {@link c4MorphProps} is the whole preset and not `{kind, role}`.
37
+ */
38
+ export const GLYPH_BODY_KINDS = new Set([
39
+ 'person',
40
+ 'person-ext',
41
+ 'database',
42
+ 'mobile',
43
+ 'browser',
44
+ ]);
45
+ /**
46
+ * Every prop a C4 shape is created with, for one kind and one box.
47
+ *
48
+ * `index` is deliberately NOT here: it is a layering statement the creation
49
+ * site makes about the five elements it is about to write in painting order,
50
+ * and it means nothing to an element that already exists.
51
+ *
52
+ * No `text` either, and that is the arrangement rather than an omission: since
53
+ * the PO's recette of 28/08/2026 the name is a `c4:title` CHILD, and the shape
54
+ * is a body and nothing else. Its text COLOUR is still seeded — an element
55
+ * drawn before that change carries its name here, and the node view routes a
56
+ * double-click to that editor for exactly those, so the words have to stay
57
+ * legible.
58
+ */
59
+ export function c4NodeProps(kind, box) {
60
+ const paint = NODE_PALETTE[kind];
61
+ const glyphBody = GLYPH_BODY_KINDS.has(kind);
62
+ return {
63
+ type: 'c4Node',
64
+ kind,
65
+ // Semantic identity, posted next to `kind` — which stays untouched and keeps
66
+ // driving the rendering. The role is the authority on what the box MEANS,
67
+ // and it is the only thing that can say so: three of the four levels are the
68
+ // same rounded rectangle (see `./roles.ts`).
69
+ role: C4_ROLE_OF_KIND[kind],
70
+ shapeType: 'rect',
71
+ // A glyph-bodied kind paints nothing natively: the head, the block and the
72
+ // cylinder are drawn by the renderer, which reads `fillColor` /
73
+ // `strokeColor` off this same model — so both stay editable from the shape
74
+ // toolbar exactly like every other node's.
75
+ filled: !glyphBody,
76
+ fillColor: paint.fill,
77
+ strokeColor: paint.border,
78
+ strokeWidth: NODE_STROKE_WIDTH,
79
+ strokeStyle: glyphBody ? StrokeStyle.None : StrokeStyle.Solid,
80
+ shapeStyle: ShapeStyle.General,
81
+ roughness: 0,
82
+ // Per kind, and mostly ZERO: the stencil draws the boxed levels as plain
83
+ // square-cornered rectangles, and rounds only the two devices. Harmless on a
84
+ // glyph-bodied kind, whose native shape is invisible.
85
+ radius: NODE_RADIUS[kind],
86
+ color: paint.text,
87
+ fontFamily: FontFamily.Inter,
88
+ fontSize: TITLE_FONT_SIZE,
89
+ textAlign: TextAlign.Center,
90
+ xywh: box.xywh,
91
+ };
92
+ }
93
+ /**
94
+ * The box {@link c4MorphProps} hands {@link c4NodeProps} and then throws away.
95
+ * Never written to a document: a morph keeps the geometry the element already
96
+ * has, and this exists only because the one builder takes a box.
97
+ */
98
+ const DISCARDED_BOX = '[0,0,0,0]';
99
+ /** What a morph must never rewrite: identity, geometry, and the user's words. */
100
+ const NOT_A_MORPH = ['type', 'xywh', 'text'];
101
+ /**
102
+ * What a kind is worth to a shape that ALREADY EXISTS — {@link c4NodeProps}
103
+ * minus the three things a morph has no business touching.
104
+ *
105
+ * ## Why the whole preset, and not `{kind, role}`
106
+ *
107
+ * Because on this pack's own table it is visibly not enough. The container
108
+ * family — `container`, `database`, `mobile`, `browser` — is four kinds where
109
+ * one paints its body natively and three hand it to the renderer, so `filled`
110
+ * and `strokeStyle` flip between them, and two of them round their corners
111
+ * where the others do not ({@link NODE_RADIUS}). Morph a container to a
112
+ * database with two keys and the cylinder arrives with the rectangle still
113
+ * painted behind it. The `-ext` families are the same argument in colour: grey
114
+ * IS what "somebody else owns this" means, and it lives in `fillColor` /
115
+ * `strokeColor`.
116
+ */
117
+ export function c4MorphProps(kind) {
118
+ // Widened to the plain record on the way in: `type` is required on what the
119
+ // creation builder returns, and `delete` may only take an optional key.
120
+ const props = {
121
+ ...c4NodeProps(kind, { xywh: DISCARDED_BOX }),
122
+ };
123
+ for (const key of NOT_A_MORPH)
124
+ delete props[key];
125
+ return props;
126
+ }
127
+ /**
128
+ * Every key ANY kind's props may carry — the union over the whole pack.
129
+ *
130
+ * Computed rather than listed, so a preset that starts spreading a key
131
+ * conditionally is covered on the day it is added rather than on the day
132
+ * somebody notices.
133
+ */
134
+ const EVERY_MORPH_KEY = new Set(Object.keys(NODE_PALETTE).flatMap(kind => Object.keys(c4MorphProps(kind))));
135
+ /**
136
+ * The fields to DELETE after morphing to `kind` — the keys some other kind
137
+ * writes and this one does not.
138
+ *
139
+ * EMPTY for every kind today, because no C4 preset spreads anything
140
+ * conditionally: all nine write the same key set with different values. Kept
141
+ * anyway, and derived rather than hard-coded to `[]`, for the reason BPMN's
142
+ * equivalent exists at all — a patch cannot express absence, and the day one
143
+ * kind stops writing a key the previous kind's value would otherwise stay in
144
+ * the Y.Map, silently in force.
145
+ */
146
+ export function c4MorphClears(kind) {
147
+ const present = new Set(Object.keys(c4MorphProps(kind)));
148
+ return [...EVERY_MORPH_KEY].filter(key => !present.has(key));
149
+ }
@@ -0,0 +1,2 @@
1
+ import type { ValidationProfile } from '@formicoidea/labre-core/blocks/surface';
2
+ export declare const C4_PROFILES: readonly ValidationProfile[];
@@ -0,0 +1,177 @@
1
+ /**
2
+ * C4 validation profiles.
3
+ *
4
+ * DATA owned by the framework, like its rules and its roles. A profile is
5
+ * chosen per FRAME — the board, and the boundary, which are the two elements
6
+ * this framework's rules measure against — and the choice rides on the element
7
+ * itself, so two diagrams at two levels of requirement coexist on one canvas:
8
+ * the board somebody is still sketching stays a sketch while the one that is
9
+ * being handed over is held to the checklist.
10
+ *
11
+ * Registered from the flag-gated `C4ViewExtension`, beside the rules:
12
+ * switching the `c4` flag off takes the choice away with the rest of the
13
+ * tooling, and a board already set to `strict` simply stops being checked until
14
+ * it comes back — the id stays written, untouched.
15
+ *
16
+ * ## Both tables spell out all SIXTEEN ids
17
+ *
18
+ * Every severity a user can get is either the one its rule declares or one of
19
+ * these lines — nothing is raised implicitly (PF9.4). Spelling them all out is
20
+ * what makes the level READABLE: a reviewer asking what `c4.strict` actually
21
+ * requires reads sixteen lines here instead of one file per rule, and a rule
22
+ * shipped later cannot join a level in silence.
23
+ */
24
+ /**
25
+ * Sketch: every rule at `audit`. Findings still reach `violations$` — a host
26
+ * panel, a check-up and a conformance report see them — and the canvas says
27
+ * nothing.
28
+ *
29
+ * The DEFAULT, and deliberately so (PRD principle 3), and more deliberately
30
+ * here than anywhere else in the library: a C4 diagram is drawn from the
31
+ * outside in. The boxes go down, then the arrows, then the words, and for the
32
+ * whole of that every element is isolated, every arrow is unlabelled, every box
33
+ * is unnamed and half the links are still plain connectors somebody
34
+ * quick-dragged. A tool arguing with that hand is a tool switched off within
35
+ * the hour, and it would be arguing about a diagram the author already knows is
36
+ * unfinished.
37
+ *
38
+ * The sketch PRIMES: the findings are computed, collected and available the
39
+ * moment the author asks — through the panel, through a check-up, through the
40
+ * profile switch — so nothing has to be re-derived when they decide the drawing
41
+ * is a deliverable. Being the default also means it WRITES NOTHING: a board on
42
+ * `sketch` carries no profile key, so every C4 diagram ever drawn is on it,
43
+ * with no migration and no backfill.
44
+ */
45
+ const sketch = {
46
+ id: 'c4.sketch',
47
+ framework: 'c4',
48
+ labelKey: 'com.labre.c4.profile.sketch',
49
+ fallback: 'Sketch',
50
+ isDefault: true,
51
+ rules: {
52
+ 'c4.unlabeled-relationship': 'audit',
53
+ 'c4.unnamed-element': 'audit',
54
+ 'c4.untyped-link': 'audit',
55
+ 'c4.relationship-endpoints': 'audit',
56
+ 'c4.relationship-self-loop': 'audit',
57
+ 'c4.isolated-system': 'audit',
58
+ 'c4.isolated-container': 'audit',
59
+ 'c4.isolated-component': 'audit',
60
+ 'c4.database-initiates': 'audit',
61
+ 'c4.homeless-component': 'audit',
62
+ 'c4.person-in-boundary': 'audit',
63
+ 'c4.system-in-boundary': 'audit',
64
+ 'c4.container-in-container-boundary': 'audit',
65
+ 'c4.component-level-skip': 'audit',
66
+ 'c4.context-diagram-level': 'audit',
67
+ 'c4.container-diagram-level': 'audit',
68
+ },
69
+ };
70
+ /**
71
+ * Strict: the diagram is a DELIVERABLE, and it is held to the review checklist.
72
+ *
73
+ * The level somebody chooses when a diagram stops being a thinking aid and
74
+ * becomes something another team will be handed. ELEVEN rules move to
75
+ * `warning`.
76
+ *
77
+ * Three of them read a question from the checklist itself (c4model.com): is
78
+ * every element named, is every relationship labelled, is every arrow one the
79
+ * model can state.
80
+ *
81
+ * Two are the LEVEL rules — `c4.context-diagram-level` and
82
+ * `c4.container-diagram-level` — and they are the easiest promotion in the
83
+ * table, because the user has already spoken: a board declaring a level is an
84
+ * author saying which sheet this is, and a container drawn on a sheet its own
85
+ * author called a context diagram is a contradiction inside one document. There
86
+ * is no reading under which the drawing meant it — and the one reading under
87
+ * which nothing should be said, a sheet whose level was never stated, is silence
88
+ * from the rules themselves rather than from this table.
89
+ *
90
+ * Three more are the ZOOM rules — `c4.system-in-boundary`,
91
+ * `c4.container-in-container-boundary` and `c4.component-level-skip`. They are
92
+ * promoted on the same test as everything else here: whether the diagram might
93
+ * honestly have meant it. None of the three can be meant. A boundary is a system
94
+ * or a container, so nothing inside one is a system; a container boundary IS its
95
+ * container, so no container goes in it; and a component framed by no container
96
+ * is a sheet with a level missing. Each is a drawing whose author will change it
97
+ * once they see it, which is the definition of a `warning` at the level where
98
+ * somebody has said the diagram is a deliverable.
99
+ *
100
+ * The other three — `c4.untyped-link`, `c4.relationship-self-loop` and
101
+ * `c4.homeless-component` — are OURS, and they are promoted anyway. Provenance
102
+ * and severity are ORTHOGONAL, and this table is where that shows: what decides
103
+ * whether a house reading bites is not where it came from but whether the
104
+ * diagram might honestly have meant it. A connector the model never recorded, a
105
+ * box drawn as using itself, a component belonging to no container — none of
106
+ * the three has a second reading, so at the level where somebody has said the
107
+ * drawing is a deliverable, saying so is the point. Each message names the rule
108
+ * as our convention rather than implying C4 forbids it, which is what keeps the
109
+ * promotion honest.
110
+ *
111
+ * Note the partition here is NOT the provenance one — eleven promoted against
112
+ * eleven `recommendation` rules is a coincidence of arithmetic and nothing else,
113
+ * since the two elevens do not hold the same rules: the three `c4.isolated-*`
114
+ * come from the checklist and stay remarks, while three of ours are promoted.
115
+ * The spec constant is called `PROMOTED` for exactly that reason.
116
+ *
117
+ * ## The five that do NOT move, and why the table spells them out
118
+ *
119
+ * Five out of sixteen, still — the two rules this slice added both moved.
120
+ *
121
+ * `c4.isolated-system`, `c4.isolated-container`, `c4.isolated-component`,
122
+ * `c4.database-initiates` and `c4.person-in-boundary` stay `audit` here too.
123
+ *
124
+ * The three isolation rules report a diagram that is UNFINISHED rather than
125
+ * wrong: an element with no arrow on it is a box whose author has not got to
126
+ * the arrows yet, and "not done" is what a panel is for. Promoting them would
127
+ * put a bracket on every box for the first minutes of every drawing, at the one
128
+ * level where the user has explicitly said the drawing matters.
129
+ *
130
+ * The other two are OURS rather than the checklist's — idioms of the notation,
131
+ * not requirements of it — and, unlike the self-loop promoted above, each has a
132
+ * reading under which the author is right. A store with a change feed really
133
+ * does push, and a person really can be drawn inside a boundary by somebody
134
+ * making a point about an operator embedded in a process. Both are worth
135
+ * remarking on and neither is something the tool should claim is a mistake, at
136
+ * any level. That is the line between a convention that bites and one that does
137
+ * not: whether the diagram might have meant it.
138
+ *
139
+ * ## Nothing is `blocking-overridable`, and nothing ever will be here
140
+ *
141
+ * Nothing in this library implements refusal — no gesture is declined anywhere
142
+ * — so declaring the level would be data claiming an effect that does not exist
143
+ * (the `wardley/rules.ts:30` promise, kept). Unlike BPMN, C4 has no rule that
144
+ * would move there the day refusal lands: a review checklist is a set of
145
+ * questions asked of a finished drawing, not a grammar that can refuse a
146
+ * gesture while it is being made.
147
+ */
148
+ const strict = {
149
+ id: 'c4.strict',
150
+ framework: 'c4',
151
+ labelKey: 'com.labre.c4.profile.strict',
152
+ fallback: 'Review checklist',
153
+ rules: {
154
+ 'c4.unlabeled-relationship': 'warning',
155
+ 'c4.unnamed-element': 'warning',
156
+ 'c4.untyped-link': 'warning',
157
+ 'c4.relationship-endpoints': 'warning',
158
+ 'c4.relationship-self-loop': 'warning',
159
+ 'c4.homeless-component': 'warning',
160
+ // The three zoom rules: a drawing that contradicts the model's own levels
161
+ // has no second reading — see the header.
162
+ 'c4.system-in-boundary': 'warning',
163
+ 'c4.container-in-container-boundary': 'warning',
164
+ 'c4.component-level-skip': 'warning',
165
+ // The two LEVEL rules: the author has SAID which sheet this is, and the
166
+ // sheet shows something that sheet does not show — see the header.
167
+ 'c4.context-diagram-level': 'warning',
168
+ 'c4.container-diagram-level': 'warning',
169
+ // The five that do not move — see the header.
170
+ 'c4.isolated-system': 'audit',
171
+ 'c4.isolated-container': 'audit',
172
+ 'c4.isolated-component': 'audit',
173
+ 'c4.database-initiates': 'audit',
174
+ 'c4.person-in-boundary': 'audit',
175
+ },
176
+ };
177
+ export const C4_PROFILES = [sketch, strict];