@formicoidea/labre-framework-ddd-context-map 0.31.0 → 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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Context Mapping validation profiles (WS2).
3
+ *
4
+ * DATA owned by the framework, like its rules and its roles. A profile is
5
+ * chosen per BOARD, not per document, and the choice rides on the board element
6
+ * itself — two maps at two levels of requirement coexist on one canvas.
7
+ *
8
+ * Registered from the flag-gated `DddContextMapViewExtension`, beside the
9
+ * rules: switching the Context Map flag off takes the choice away with the rest
10
+ * of the tooling, and a board already set to `strict` simply stops being checked
11
+ * until it comes back — the id stays written, untouched.
12
+ */
13
+ /**
14
+ * Sketch: every rule demoted to `audit`. Findings still reach `violations$` —
15
+ * a host panel and a conformance report see them — and the canvas says nothing.
16
+ *
17
+ * The DEFAULT, and deliberately so (PRD principle 3). A context map is drawn in
18
+ * a workshop, at speed, with half the links pointing the wrong way while
19
+ * somebody remembers which team is upstream; a tool arguing with that hand is a
20
+ * tool switched off within the hour. Being the default also means it WRITES
21
+ * NOTHING: a board on `sketch` carries no profile key, so every map ever drawn
22
+ * is on it, with no migration and no backfill.
23
+ */
24
+ const sketch = {
25
+ id: 'context-map.sketch',
26
+ framework: 'ddd-context-map',
27
+ labelKey: 'com.labre.ddd-context-map.profile.sketch',
28
+ fallback: 'Sketch',
29
+ isDefault: true,
30
+ rules: {
31
+ 'context-map.relationship-endpoints': 'audit',
32
+ 'context-map.acl-conformist-exclusive': 'audit',
33
+ 'context-map.pattern-on-customer-supplier': 'audit',
34
+ 'context-map.acl-on-customer-supplier': 'audit',
35
+ 'context-map.context-off-board': 'audit',
36
+ },
37
+ };
38
+ /**
39
+ * Strict: the deliverable level — a map that leaves the room. Every severity is
40
+ * spelled out rather than left absent, so the answer is readable in one place.
41
+ *
42
+ * ## The one that does not move
43
+ *
44
+ * `acl-on-customer-supplier` stays `audit` HERE TOO, and it is the reason a
45
+ * profile spells everything out. It is not a softer version of a contradiction,
46
+ * it is a different KIND of statement: the combination is legitimate — a team
47
+ * negotiating its needs upstream while still translating a model that is being
48
+ * retired — and only the team knows which case they are in. Promoting it with
49
+ * the others would turn a good question into a false accusation on every mature
50
+ * map in the corpus.
51
+ *
52
+ * Nothing here is `blocking-overridable`: nothing in this library implements
53
+ * refusal, and declaring it would be data claiming an effect that does not
54
+ * exist.
55
+ */
56
+ const strict = {
57
+ id: 'context-map.strict',
58
+ framework: 'ddd-context-map',
59
+ labelKey: 'com.labre.ddd-context-map.profile.strict',
60
+ fallback: 'Strict',
61
+ rules: {
62
+ 'context-map.relationship-endpoints': 'warning',
63
+ 'context-map.acl-conformist-exclusive': 'warning',
64
+ 'context-map.pattern-on-customer-supplier': 'warning',
65
+ // See the header: a judgement the map cannot make at any level.
66
+ 'context-map.acl-on-customer-supplier': 'audit',
67
+ 'context-map.context-off-board': 'warning',
68
+ },
69
+ };
70
+ export const CONTEXT_MAP_PROFILES = [
71
+ sketch,
72
+ strict,
73
+ ];
@@ -0,0 +1,65 @@
1
+ import { CM_RELATIONSHIPS } from '@formicoidea/labre-ddd-shared';
2
+ import type { RoleDefs } from '@formicoidea/labre-core/std/gfx';
3
+ /**
4
+ * Context Mapping role vocabulary (WS2).
5
+ *
6
+ * Roles are the semantic identity of a Context Map artefact — no rule will ever
7
+ * look at a shape type. Three families:
8
+ *
9
+ * - the **board** (`context-map:board`), the white card the map is drawn on;
10
+ * - the **bounded context** (`context-map:context`), the blue pill;
11
+ * - the **relationship** (`context-map:relationship`), the edge parent, with
12
+ * one child per DDD pattern.
13
+ *
14
+ * ## One role per pattern, and why
15
+ *
16
+ * The PO arbitration of 26/08/2026: a relationship is not a connector with a
17
+ * `pattern` property, it is nine different edges. Two reasons, both of them
18
+ * about what the rules can then say. A pattern-per-role means a rule is written
19
+ * on the pattern it is about (`acl` and `conformist` may not coexist) without
20
+ * the engine growing a way to read a property; and it means a combination the
21
+ * notation genuinely allows — C/S plus OHS plus PL on the same couple, which is
22
+ * DDD Crew's own example — is drawn as PARALLEL LINKS, each a statement of its
23
+ * own, rather than as a bag of flags nobody can point at.
24
+ *
25
+ * Hierarchy is DATA (`parent`): every pattern specialises
26
+ * `context-map:relationship`, so a rule written on the parent — the endpoint
27
+ * grammar, the self-loop ban — covers all nine for free (see `roleIsA`).
28
+ *
29
+ * ## `direction` only on the U/D patterns
30
+ *
31
+ * Tier 1 of `docs/adr/0010` says an edge's source is the subject of its verb.
32
+ * That only MEANS something for the five upstream/downstream patterns, where
33
+ * the notation itself has a direction: the source is the upstream context. The
34
+ * four symmetric ones (Partnership, Shared Kernel, Separate Ways, Big Ball of
35
+ * Mud) relate two contexts as equals — declaring a verb on them would announce a
36
+ * gesture ("drag from the upstream one") that means nothing, and the reveal (M2)
37
+ * would show a sentence the framework does not say. They therefore declare none,
38
+ * which is exactly what an untyped edge already did.
39
+ *
40
+ * ## Compat
41
+ *
42
+ * Nothing is backfilled. Relationships drawn before these roles existed — the
43
+ * old free connector + tag + U/D markers group — carry no role and are never
44
+ * evaluated (promesse #71): they stay drawings, in the documents they are in.
45
+ */
46
+ /** The nine DDD Crew patterns, by the `kind` used at the creation sites. */
47
+ export type ContextMapPatternKind = (typeof CM_RELATIONSHIPS)[number]['kind'];
48
+ export type ContextMapRoleId = `context-map:${string}`;
49
+ export declare const CM_PATTERN_ROLE: Record<ContextMapPatternKind, ContextMapRoleId>;
50
+ /** The three roles that are not a pattern, plus the pattern ids. */
51
+ export declare const CONTEXT_MAP_ROLE: {
52
+ readonly partnership: `context-map:${string}`;
53
+ readonly sharedKernel: `context-map:${string}`;
54
+ readonly customerSupplier: `context-map:${string}`;
55
+ readonly conformist: `context-map:${string}`;
56
+ readonly acl: `context-map:${string}`;
57
+ readonly ohs: `context-map:${string}`;
58
+ readonly publishedLanguage: `context-map:${string}`;
59
+ readonly separateWays: `context-map:${string}`;
60
+ readonly bbom: `context-map:${string}`;
61
+ readonly board: "context-map:board";
62
+ readonly context: "context-map:context";
63
+ readonly relationship: "context-map:relationship";
64
+ };
65
+ export declare const CONTEXT_MAP_ROLES: RoleDefs;
package/dist/roles.js ADDED
@@ -0,0 +1,64 @@
1
+ import { CM_RELATIONSHIPS } from '@formicoidea/labre-ddd-shared';
2
+ /**
3
+ * Role id per pattern kind, DERIVED from the shared preset table rather than
4
+ * restated: the kinds are camelCase (`sharedKernel`) and the role ids are
5
+ * kebab-case (`context-map:shared-kernel`), and the day a tenth pattern lands in
6
+ * `CM_RELATIONSHIPS` it gets its role here with no edit.
7
+ */
8
+ const kebab = (kind) => kind.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
9
+ export const CM_PATTERN_ROLE = Object.fromEntries(CM_RELATIONSHIPS.map(preset => [
10
+ preset.kind,
11
+ `context-map:${kebab(preset.kind)}`,
12
+ ]));
13
+ /** The three roles that are not a pattern, plus the pattern ids. */
14
+ export const CONTEXT_MAP_ROLE = {
15
+ board: 'context-map:board',
16
+ context: 'context-map:context',
17
+ relationship: 'context-map:relationship',
18
+ ...CM_PATTERN_ROLE,
19
+ };
20
+ /** The DDD Crew wording, verbatim: `Upstream` is the source of the relation. */
21
+ const UPSTREAM_DIRECTION = {
22
+ verbKey: 'com.labre.ddd-context-map.role.relationship.verb',
23
+ verbFallback: 'is upstream of',
24
+ gestureHintKey: 'com.labre.ddd-context-map.role.relationship.gesture',
25
+ gestureHintFallback: 'Drag from the upstream context to the downstream one.',
26
+ };
27
+ const DEFS = [
28
+ // The board: a frame, not a context. It specialises nothing, so a rule
29
+ // written on `context-map:context` can never match the card its subjects
30
+ // sit on.
31
+ {
32
+ id: CONTEXT_MAP_ROLE.board,
33
+ kind: 'node',
34
+ labelKey: 'com.labre.ddd-context-map.role.board',
35
+ labelFallback: 'Context map',
36
+ },
37
+ {
38
+ id: CONTEXT_MAP_ROLE.context,
39
+ kind: 'node',
40
+ labelKey: 'com.labre.ddd-context-map.role.context',
41
+ labelFallback: 'Bounded context',
42
+ },
43
+ // The edge parent. Carries no `direction` of its own: five of its nine
44
+ // children have one and four do not, and a verb on the parent would be
45
+ // inherited by the symmetric patterns that deny having one.
46
+ {
47
+ id: CONTEXT_MAP_ROLE.relationship,
48
+ kind: 'edge',
49
+ labelKey: 'com.labre.ddd-context-map.role.relationship',
50
+ labelFallback: 'Relationship',
51
+ },
52
+ ...CM_RELATIONSHIPS.map((preset) => ({
53
+ id: CM_PATTERN_ROLE[preset.kind],
54
+ parent: CONTEXT_MAP_ROLE.relationship,
55
+ kind: 'edge',
56
+ labelKey: `com.labre.ddd-context-map.role.${kebab(preset.kind)}`,
57
+ labelFallback: preset.label,
58
+ // See the header: only the upstream/downstream patterns have a subject.
59
+ ...(preset.upDown ? { direction: UPSTREAM_DIRECTION } : {}),
60
+ })),
61
+ ];
62
+ // Null prototype: this is a lookup table keyed by ids that may one day come
63
+ // from host-supplied packs, so `defs['toString']` must not resolve.
64
+ export const CONTEXT_MAP_ROLES = Object.assign(Object.create(null), Object.fromEntries(DEFS.map(def => [def.id, def])));
@@ -0,0 +1,2 @@
1
+ import type { ValidationRule } from '@formicoidea/labre-core/blocks/surface';
2
+ export declare const CONTEXT_MAP_RULES: readonly ValidationRule[];
package/dist/rules.js ADDED
@@ -0,0 +1,264 @@
1
+ import { CONTEXT_MAP_BACKGROUND } from './background.js';
2
+ import { CONTEXT_MAP_ROLE, CONTEXT_MAP_ROLES } from './roles.js';
3
+ /**
4
+ * Context Mapping validation rules (WS2).
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. Registered from the flag-gated `DddContextMapViewExtension`,
9
+ * so switching the Context Map flag off removes them with the rest of the
10
+ * tooling — maps already drawn keep rendering, they simply stop being checked
11
+ * (`docs/adr/0009`).
12
+ *
13
+ * ## What the grammar of a context map actually is
14
+ *
15
+ * Very little, and that is the point. A context map has no axes: nothing about
16
+ * WHERE a context sits means anything, so there is no order rule, no zone rule
17
+ * and no orientation rule to write. What it does have is a notation with
18
+ * CONTRADICTIONS in it — patterns that cannot both be true of the same couple —
19
+ * and those are exactly what the `relation-endpoints` family arbitrates.
20
+ *
21
+ * Four of the five rules below are therefore one family with four different
22
+ * questions, and every one of them stays silent on a sketch: an end that is not
23
+ * a bounded context (a cloud, a note, a plain rectangle) is outside the
24
+ * alphabet, and the whole edge goes unjudged.
25
+ */
26
+ /**
27
+ * **CM1** — a relationship runs between two bounded contexts, once, and never
28
+ * onto itself.
29
+ *
30
+ * The base grammar, written on the PARENT edge role, which is what makes it one
31
+ * rule instead of nine: `roleIsA` resolves a triplet declared on
32
+ * `context-map:relationship` for every pattern that specialises it, so the
33
+ * single sanctioned sentence below — a context relates to a context — covers
34
+ * Partnership and Big Ball of Mud alike, and a tenth pattern inherits it on the
35
+ * day it is added.
36
+ *
37
+ * The two things it says:
38
+ *
39
+ * - **self-loop** — a context is not related to itself. Whatever the hand meant,
40
+ * the notation has no sentence for it;
41
+ * - **duplicate** — the same pattern drawn twice between the same two contexts,
42
+ * the same way round. Not two DIFFERENT patterns, which is legitimate and how
43
+ * C/S + OHS + PL is drawn; the same one, twice, which is one link too many on
44
+ * a map that is read by counting links.
45
+ *
46
+ * ## What `allowed` does here, and what it deliberately does NOT
47
+ *
48
+ * The matrix holds ONE sentence, and `context-map:context` is therefore the
49
+ * whole alphabet of this rule. That makes the off-matrix branch structurally
50
+ * unreachable — an end that is a context passes, and an end that is anything
51
+ * else is outside the alphabet, which is TOTAL silence for the edge (see
52
+ * {@link RelationEndpointsDef.allowed}).
53
+ *
54
+ * That is the intent, not an oversight. The matrix is here to declare the
55
+ * alphabet, and the alphabet is what makes the whole rule proportionate: a
56
+ * relationship drawn onto a CLOUD, onto a note, onto a rectangle somebody
57
+ * dropped on the board to think with, is a draft — the user is saying "there is
58
+ * something out there we integrate with", which on a context map is not merely
59
+ * tolerable but the correct notation. Judging it would be indicting the act of
60
+ * sketching (PRD principle 8).
61
+ *
62
+ * The branch becomes reachable the day a second NODE role lands — a role on the
63
+ * cloud, say — and on that day this declaration already says what to do with it
64
+ * without a line changing. Until then `offMatrix` carries no words of its own
65
+ * and the rule's own sentence stands behind it, unused.
66
+ */
67
+ const relationshipEndpoints = {
68
+ id: 'context-map.relationship-endpoints',
69
+ framework: 'ddd-context-map',
70
+ family: 'relation-endpoints',
71
+ severity: 'warning',
72
+ // No `appliesTo`: the subject is a RELATION, and the role that names it is
73
+ // declared where the family reads it — naming one of the three indicted
74
+ // elements here would be data that lies.
75
+ roles: CONTEXT_MAP_ROLES,
76
+ messageKey: 'com.labre.ddd-context-map.validation.relationship-endpoints',
77
+ messageFallback: 'This relationship does not run between two bounded contexts.',
78
+ suggestionKey: 'com.labre.ddd-context-map.validation.relationship-endpoints.suggestion',
79
+ suggestionFallback: 'A context map relates bounded contexts — re-point the loose end onto one, or delete the link.',
80
+ version: 1,
81
+ provenance: {
82
+ source: 'recommendation',
83
+ reference: 'Context Mapping (DDD) — the relationship grammar of the notation',
84
+ },
85
+ backgroundRole: CONTEXT_MAP_ROLE.board,
86
+ background: CONTEXT_MAP_BACKGROUND,
87
+ endpoints: {
88
+ edgeRole: CONTEXT_MAP_ROLE.relationship,
89
+ allowed: [
90
+ {
91
+ source: CONTEXT_MAP_ROLE.context,
92
+ edge: CONTEXT_MAP_ROLE.relationship,
93
+ target: CONTEXT_MAP_ROLE.context,
94
+ },
95
+ ],
96
+ forbidSelfLoop: true,
97
+ forbidDuplicate: true,
98
+ selfLoop: {
99
+ messageKey: 'com.labre.ddd-context-map.validation.relationship-self-loop',
100
+ messageFallback: 'This relationship loops back onto its own context.',
101
+ suggestionKey: 'com.labre.ddd-context-map.validation.relationship-self-loop.suggestion',
102
+ suggestionFallback: 'A bounded context integrates with OTHER contexts — drop the loose end on the context it actually relates to, or delete the link.',
103
+ },
104
+ duplicate: {
105
+ messageKey: 'com.labre.ddd-context-map.validation.relationship-duplicate',
106
+ messageFallback: 'This pattern is already drawn between these two contexts.',
107
+ suggestionKey: 'com.labre.ddd-context-map.validation.relationship-duplicate.suggestion',
108
+ suggestionFallback: 'Delete the copy. Two DIFFERENT patterns on one couple are fine — C/S with OHS and PL is the notation working — but the same one twice says nothing new.',
109
+ },
110
+ },
111
+ };
112
+ /**
113
+ * **CM2** — a context does not both conform to its upstream and protect itself
114
+ * from it.
115
+ *
116
+ * The textbook contradiction, and the one every context map workshop produces
117
+ * at least once. Conformist means "we take their model as it is"; an
118
+ * Anticorruption Layer means "we translate their model into ours because we
119
+ * refuse to take it as it is". Both are legitimate answers to the same pressure
120
+ * and they are opposite answers, so a couple carrying both has not decided —
121
+ * which is the finding.
122
+ *
123
+ * A `warning` and never a refusal: the map is a discussion aid, and the pair
124
+ * showing up on it is usually the moment the discussion starts. The user
125
+ * resolves it by deleting one, and only they know which.
126
+ */
127
+ const aclConformistExclusive = {
128
+ id: 'context-map.acl-conformist-exclusive',
129
+ framework: 'ddd-context-map',
130
+ family: 'relation-endpoints',
131
+ severity: 'warning',
132
+ roles: CONTEXT_MAP_ROLES,
133
+ messageKey: 'com.labre.ddd-context-map.validation.acl-conformist',
134
+ messageFallback: 'These two contexts are related by both a Conformist and an Anticorruption Layer.',
135
+ suggestionKey: 'com.labre.ddd-context-map.validation.acl-conformist.suggestion',
136
+ suggestionFallback: 'Conforming means taking the upstream model as it is; an ACL means refusing to. Keep the one the team actually does.',
137
+ version: 1,
138
+ provenance: {
139
+ source: 'recommendation',
140
+ reference: 'Context Mapping (DDD) — the pattern canon: an anticorruption layer and a conformist are opposite answers',
141
+ },
142
+ backgroundRole: CONTEXT_MAP_ROLE.board,
143
+ background: CONTEXT_MAP_BACKGROUND,
144
+ endpoints: {
145
+ edgeRole: CONTEXT_MAP_ROLE.relationship,
146
+ // No `allowed`: CM1 already judges the sentences, and repeating the matrix
147
+ // here would report one mistake twice. This rule asks the pair question and
148
+ // nothing else.
149
+ exclusivePairs: [[CONTEXT_MAP_ROLE.acl, CONTEXT_MAP_ROLE.conformist]],
150
+ },
151
+ };
152
+ /**
153
+ * **CM3** — Customer/Supplier already says how the two teams work; Conformist
154
+ * and Open Host Service say something else.
155
+ *
156
+ * C/S is a RELATIONSHIP OF NEGOTIATION: downstream is a customer whose needs
157
+ * enter the upstream team's backlog. Conformist is its negation — downstream
158
+ * has no say and takes what comes. OHS is a different answer again: upstream
159
+ * serves all comers through a published protocol precisely so it does NOT have
160
+ * to negotiate per consumer. Either one drawn on top of a C/S says the couple
161
+ * has not settled which conversation it is having.
162
+ *
163
+ * ACL is NOT in this pair list, on purpose — see {@link aclOnCustomerSupplier},
164
+ * where it is an audit finding rather than a warning.
165
+ */
166
+ const patternOnCustomerSupplier = {
167
+ id: 'context-map.pattern-on-customer-supplier',
168
+ framework: 'ddd-context-map',
169
+ family: 'relation-endpoints',
170
+ severity: 'warning',
171
+ roles: CONTEXT_MAP_ROLES,
172
+ messageKey: 'com.labre.ddd-context-map.validation.pattern-on-customer-supplier',
173
+ messageFallback: 'This Customer/Supplier relationship carries a second pattern that contradicts it.',
174
+ suggestionKey: 'com.labre.ddd-context-map.validation.pattern-on-customer-supplier.suggestion',
175
+ suggestionFallback: 'Customer/Supplier means the downstream needs are negotiated into the upstream backlog. Conformist says they are not; Open Host Service says the upstream serves everyone the same way. Keep one.',
176
+ version: 1,
177
+ provenance: {
178
+ source: 'recommendation',
179
+ reference: 'Context Mapping (DDD) — Customer/Supplier is a relationship of negotiation',
180
+ },
181
+ backgroundRole: CONTEXT_MAP_ROLE.board,
182
+ background: CONTEXT_MAP_BACKGROUND,
183
+ endpoints: {
184
+ edgeRole: CONTEXT_MAP_ROLE.relationship,
185
+ exclusivePairs: [
186
+ [CONTEXT_MAP_ROLE.customerSupplier, CONTEXT_MAP_ROLE.conformist],
187
+ [CONTEXT_MAP_ROLE.customerSupplier, CONTEXT_MAP_ROLE.ohs],
188
+ ],
189
+ },
190
+ };
191
+ /**
192
+ * **CM4** — an Anticorruption Layer on a Customer/Supplier is worth a second
193
+ * look, and nothing more.
194
+ *
195
+ * Deliberately `audit`, alone among the four. The combination is not wrong: a
196
+ * downstream team can perfectly well negotiate its needs upstream AND still
197
+ * translate the upstream model at its border, and mature teams do exactly that
198
+ * while a legacy model is being retired. But it is also, often, the trace of a
199
+ * negotiation that has stopped working — the customer built a translation layer
200
+ * because the supplier stopped listening.
201
+ *
202
+ * A tool cannot tell those two apart, so it does not get to interrupt: the
203
+ * finding reaches `violations$` for a host panel and a conformance report, and
204
+ * the canvas says nothing. `strict` leaves it at `audit` too, which is the whole
205
+ * reason a profile spells every severity out — this one is a judgement the map
206
+ * cannot make at any level of requirement.
207
+ */
208
+ const aclOnCustomerSupplier = {
209
+ id: 'context-map.acl-on-customer-supplier',
210
+ framework: 'ddd-context-map',
211
+ family: 'relation-endpoints',
212
+ severity: 'audit',
213
+ roles: CONTEXT_MAP_ROLES,
214
+ messageKey: 'com.labre.ddd-context-map.validation.acl-on-customer-supplier',
215
+ messageFallback: 'This Customer/Supplier relationship also carries an Anticorruption Layer.',
216
+ suggestionKey: 'com.labre.ddd-context-map.validation.acl-on-customer-supplier.suggestion',
217
+ suggestionFallback: 'Legitimate while a model is being retired — worth asking whether the negotiation still works if it is not.',
218
+ version: 1,
219
+ provenance: {
220
+ source: 'labre-convention',
221
+ reference: 'Labre convention — the notation forbids nothing here; a tool cannot tell the two readings apart',
222
+ },
223
+ backgroundRole: CONTEXT_MAP_ROLE.board,
224
+ background: CONTEXT_MAP_BACKGROUND,
225
+ endpoints: {
226
+ edgeRole: CONTEXT_MAP_ROLE.relationship,
227
+ exclusivePairs: [[CONTEXT_MAP_ROLE.customerSupplier, CONTEXT_MAP_ROLE.acl]],
228
+ },
229
+ };
230
+ /**
231
+ * **CM5** — a bounded context belongs on the map.
232
+ *
233
+ * The one geometric rule the framework has, and it is about membership rather
234
+ * than position: a context parked beside the card is not on the map, so nothing
235
+ * the map says covers it. Silent when the board carries no board element at all
236
+ * — a context map sketched on the bare canvas is a sketch, not an error, and so
237
+ * is one drawn before the board existed.
238
+ */
239
+ const contextOffBoard = {
240
+ id: 'context-map.context-off-board',
241
+ framework: 'ddd-context-map',
242
+ family: 'element-in-background',
243
+ severity: 'warning',
244
+ appliesTo: CONTEXT_MAP_ROLE.context,
245
+ roles: CONTEXT_MAP_ROLES,
246
+ messageKey: 'com.labre.ddd-context-map.validation.context-off-board',
247
+ messageFallback: 'This bounded context sits outside the map.',
248
+ suggestionKey: 'com.labre.ddd-context-map.validation.context-off-board.suggestion',
249
+ suggestionFallback: 'Drag it onto the board, or grow the board to take it in.',
250
+ version: 1,
251
+ provenance: {
252
+ source: 'labre-convention',
253
+ reference: 'Labre convention — membership on this canvas, not a Context Mapping rule',
254
+ },
255
+ backgroundRole: CONTEXT_MAP_ROLE.board,
256
+ background: CONTEXT_MAP_BACKGROUND,
257
+ };
258
+ export const CONTEXT_MAP_RULES = [
259
+ relationshipEndpoints,
260
+ aclConformistExclusive,
261
+ patternOnCustomerSupplier,
262
+ aclOnCustomerSupplier,
263
+ contextOffBoard,
264
+ ];
@@ -0,0 +1,30 @@
1
+ import { type ToolbarContext } from '@formicoidea/labre-core/shared/services';
2
+ /**
3
+ * The selected board's contextual toolbar: the resize toggle, and the automatic
4
+ * legend of what is actually drawn on the board. Registered ALWAYS-ON
5
+ * (`DddContextMapRenderViewExtension`) — a stored board must stay usable with
6
+ * the Context Map button switched off (`docs/adr/0009`), legend included: a
7
+ * legend is real editable elements, so generating one is authoring a document,
8
+ * not tooling that a flag may take away.
9
+ *
10
+ * This is THE legend of the module. The palette used to also offer a static,
11
+ * full-notation one; the PO's recette (27/08/2026) removed it — two legends were
12
+ * two answers to one question, and the honest answer is what is actually drawn.
13
+ * Same single-gesture shape as Core Domain Chart.
14
+ */
15
+ export declare const contextMapBoardToolbarConfig: {
16
+ readonly actions: [{
17
+ readonly id: "a.toggle-resize";
18
+ readonly tooltip: "Enable / lock resizing";
19
+ readonly icon: import("lit-html").TemplateResult<1>;
20
+ readonly active: (ctx: ToolbarContext) => boolean;
21
+ readonly run: (ctx: ToolbarContext) => void;
22
+ }, {
23
+ readonly id: "b.legend";
24
+ readonly tooltip: "Generate the legend (notation present)";
25
+ readonly icon: import("lit-html").TemplateResult<1>;
26
+ readonly run: (ctx: ToolbarContext) => void;
27
+ }];
28
+ readonly when: (ctx: ToolbarContext) => boolean;
29
+ };
30
+ export declare const contextMapBoardToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
@@ -0,0 +1,87 @@
1
+ import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
2
+ import { createAutoLegend, dddLegendIcon } from '@formicoidea/labre-ddd-shared';
3
+ import { ContextMapBoardElementModel } from '@formicoidea/labre-core/model';
4
+ import { TelemetryProvider, ToolbarModuleExtension, } from '@formicoidea/labre-core/shared/services';
5
+ import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
6
+ import { html } from 'lit';
7
+ import { CONTEXT_MAP_AUTO_LEGEND } from '../legend.js';
8
+ const ResizeIcon = html `<svg
9
+ width="24"
10
+ height="24"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ stroke-width="1.6"
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ >
18
+ <path d="M9 5H5v4M15 19h4v-4" />
19
+ <path d="M5 5l6 6M19 19l-6-6" />
20
+ </svg>`;
21
+ /**
22
+ * The selected board's contextual toolbar: the resize toggle, and the automatic
23
+ * legend of what is actually drawn on the board. Registered ALWAYS-ON
24
+ * (`DddContextMapRenderViewExtension`) — a stored board must stay usable with
25
+ * the Context Map button switched off (`docs/adr/0009`), legend included: a
26
+ * legend is real editable elements, so generating one is authoring a document,
27
+ * not tooling that a flag may take away.
28
+ *
29
+ * This is THE legend of the module. The palette used to also offer a static,
30
+ * full-notation one; the PO's recette (27/08/2026) removed it — two legends were
31
+ * two answers to one question, and the honest answer is what is actually drawn.
32
+ * Same single-gesture shape as Core Domain Chart.
33
+ */
34
+ export const contextMapBoardToolbarConfig = {
35
+ actions: [
36
+ {
37
+ id: 'a.toggle-resize',
38
+ tooltip: 'Enable / lock resizing',
39
+ icon: ResizeIcon,
40
+ active(ctx) {
41
+ const models = ctx.getSurfaceModelsByType(ContextMapBoardElementModel);
42
+ return models.length > 0 && models.every(model => model.resizeEnabled);
43
+ },
44
+ run(ctx) {
45
+ const models = ctx.getSurfaceModelsByType(ContextMapBoardElementModel);
46
+ if (!models.length)
47
+ return;
48
+ const enable = !models.every(model => model.resizeEnabled);
49
+ ctx.std.store.captureSync();
50
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
51
+ for (const model of models) {
52
+ crud.updateElement(model.id, { resizeEnabled: enable });
53
+ }
54
+ },
55
+ },
56
+ {
57
+ id: 'b.legend',
58
+ tooltip: 'Generate the legend (notation present)',
59
+ icon: dddLegendIcon,
60
+ run(ctx) {
61
+ const board = ctx.getSurfaceModelsByType(ContextMapBoardElementModel)[0];
62
+ if (!board)
63
+ return;
64
+ createAutoLegend(ctx.std, board, CONTEXT_MAP_AUTO_LEGEND);
65
+ ctx.std
66
+ .getOptional(TelemetryProvider)
67
+ ?.track('FrameworkLegendCreated', {
68
+ // The WIRE value, which is not the module id: the framework is
69
+ // `ddd-context-map` in code and `context-map` in PostHog
70
+ // (`frameworks.ts` `telemetryKey`, and the only value
71
+ // `FrameworkElementEvent` accepts). Same convention as Wardley's own
72
+ // legend button, so the two are comparable.
73
+ framework: 'context-map',
74
+ element: 'legend',
75
+ page: 'whiteboard editor',
76
+ segment: 'element toolbar',
77
+ module: 'context-map toolbar',
78
+ });
79
+ },
80
+ },
81
+ ],
82
+ when: (ctx) => ctx.getSurfaceModelsByType(ContextMapBoardElementModel).length > 0,
83
+ };
84
+ export const contextMapBoardToolbarExtension = ToolbarModuleExtension({
85
+ id: BlockFlavourIdentifier('affine:surface:contextMap'),
86
+ config: contextMapBoardToolbarConfig,
87
+ });
@@ -1,10 +1,12 @@
1
- import { DddMenuBase } from '@formicoidea/labre-ddd-shared';
2
- /** Context Map palette: bounded-context bubble, the cloud, the nine relationship patterns. */
3
- export declare class EdgelessDddContextMapMenu extends DddMenuBase {
4
- protected framework: "context-map";
5
- private _createBubble;
6
- private _createCloud;
7
- private _createRelationship;
8
- private _createLegend;
9
- render(): import("lit-html").TemplateResult<1>;
1
+ import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
2
+ import { EdgelessCommandMenu } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
3
+ /**
4
+ * Context Map palette, rendered from `contextMapCommands` for the
5
+ * `senior-menu` surface (`docs/adr/0008`). Keeps the DDD palettes' tighter gap
6
+ * — it is the widest one, at 13 buttons.
7
+ */
8
+ export declare class EdgelessDddContextMapMenu extends EdgelessCommandMenu {
9
+ static styles: import("lit").CSSResultGroup[];
10
+ protected owner: "ddd-context-map";
11
+ type: typeof EmptyTool;
10
12
  }