@formicoidea/labre-framework-ddd-context-map 0.32.0 → 0.34.1

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/legend.js ADDED
@@ -0,0 +1,56 @@
1
+ import { CM_BUBBLE, CM_RELATIONSHIPS, LABEL_COLOR, roleLabel, } from '@formicoidea/labre-ddd-shared';
2
+ import { CM_PATTERN_ROLE, CONTEXT_MAP_ROLE, CONTEXT_MAP_ROLES } from './roles.js';
3
+ /**
4
+ * What the Context Map board's automatic legend can say — a TABLE, and nothing
5
+ * else: the scan, the placement and the box are `createAutoLegend`'s job.
6
+ *
7
+ * Every row is DERIVED from the presets the palette itself draws from
8
+ * ({@link CM_RELATIONSHIPS}, {@link CM_BUBBLE}) and from the role vocabulary's
9
+ * own labels, never restated. A tenth pattern added to `CM_RELATIONSHIPS` gets
10
+ * its legend row here with no edit, the same way it gets its role.
11
+ *
12
+ * ## What is missing, and why
13
+ *
14
+ * The **cloud** (the "System / Big Ball of Mud" blob) carries no role: it is
15
+ * created neutral by `commands.ts`, deliberately — the endpoint grammar treats a
16
+ * relationship drawn onto a cloud as a sketch and stays silent on it. Detection
17
+ * is by role and only by role, so the automatic legend cannot list the cloud and
18
+ * does not pretend to. The palette's static Legend entry still documents it, and
19
+ * the day the cloud earns a role its row lands here in one line.
20
+ */
21
+ export const CONTEXT_MAP_AUTO_LEGEND = {
22
+ title: 'Legend',
23
+ width: 290,
24
+ roles: CONTEXT_MAP_ROLES,
25
+ sections: [
26
+ {
27
+ title: 'Boundaries',
28
+ entries: [
29
+ {
30
+ role: CONTEXT_MAP_ROLE.context,
31
+ row: {
32
+ swatch: 'square',
33
+ color: CM_BUBBLE.fill,
34
+ label: roleLabel(CONTEXT_MAP_ROLES, CONTEXT_MAP_ROLE.context),
35
+ },
36
+ },
37
+ ],
38
+ },
39
+ {
40
+ title: 'Relationships',
41
+ // One entry per PATTERN, not one for `context-map:relationship`: a legend
42
+ // that said "Relationship" would document nothing a reader could use. The
43
+ // dashed sample marks the two "no real integration" patterns, exactly as
44
+ // the board draws them.
45
+ entries: CM_RELATIONSHIPS.map(preset => ({
46
+ role: CM_PATTERN_ROLE[preset.kind],
47
+ row: {
48
+ swatch: 'line',
49
+ color: LABEL_COLOR,
50
+ dashed: preset.dashed,
51
+ label: `${preset.abbrev} — ${preset.label}`,
52
+ },
53
+ })),
54
+ },
55
+ ],
56
+ };
@@ -0,0 +1,19 @@
1
+ import type { QualityNudge } from '@formicoidea/labre-core/blocks/surface';
2
+ /**
3
+ * Context Mapping **map quality** — the checklist (WS2, level 2).
4
+ *
5
+ * `./rules.ts` is level 1: deterministic, decidable, real time. This file is
6
+ * everything a context map needs that no algorithm can decide. The split is the
7
+ * taxonomy's whole point — ticking is assuming, never verifying.
8
+ *
9
+ * Every one of the four is about whether a PATTERN WAS CHOSEN rather than
10
+ * defaulted to. That is the failure mode of context mapping in practice: the
11
+ * team draws nine links because there are nine integrations, picks the arrow
12
+ * that looks right, and ships a map that records the current wiring instead of
13
+ * the relationships. Nothing on the canvas can tell those two maps apart.
14
+ *
15
+ * Registered from the flag-gated `DddContextMapViewExtension`: a checklist is
16
+ * tooling. Switching the flag off takes it away and leaves the ticks written on
17
+ * the board, unread, until it comes back.
18
+ */
19
+ export declare const CONTEXT_MAP_NUDGES: readonly QualityNudge[];
package/dist/nudges.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Context Mapping **map quality** — the checklist (WS2, level 2).
3
+ *
4
+ * `./rules.ts` is level 1: deterministic, decidable, real time. This file is
5
+ * everything a context map needs that no algorithm can decide. The split is the
6
+ * taxonomy's whole point — ticking is assuming, never verifying.
7
+ *
8
+ * Every one of the four is about whether a PATTERN WAS CHOSEN rather than
9
+ * defaulted to. That is the failure mode of context mapping in practice: the
10
+ * team draws nine links because there are nine integrations, picks the arrow
11
+ * that looks right, and ships a map that records the current wiring instead of
12
+ * the relationships. Nothing on the canvas can tell those two maps apart.
13
+ *
14
+ * Registered from the flag-gated `DddContextMapViewExtension`: a checklist is
15
+ * tooling. Switching the flag off takes it away and leaves the ticks written on
16
+ * the board, unread, until it comes back.
17
+ */
18
+ export const CONTEXT_MAP_NUDGES = [
19
+ {
20
+ id: 'context-map.q1-pattern-discussed',
21
+ framework: 'ddd-context-map',
22
+ labelKey: 'com.labre.ddd-context-map.quality.pattern-discussed',
23
+ fallback: 'Every relationship carries a justified, discussed pattern — not the one that was easiest to draw.',
24
+ order: 1,
25
+ },
26
+ {
27
+ id: 'context-map.q2-separate-ways-documented',
28
+ framework: 'ddd-context-map',
29
+ labelKey: 'com.labre.ddd-context-map.quality.separate-ways',
30
+ fallback: 'Separate Ways are documented: it is written down why there is no integration.',
31
+ order: 2,
32
+ },
33
+ {
34
+ /**
35
+ * The rule this nudge replaces. "Every downstream of a Big Ball of Mud is
36
+ * protected" is decidable in principle — walk the edges out of the cloud and
37
+ * look for an ACL — and undecidable in practice on THIS canvas: the cloud
38
+ * carries no role (a v1 cut), so there is nothing for a rule to walk from,
39
+ * and a shape somebody drew a mess with is not a claim that it IS one.
40
+ * Level 2 is the honest home for it until the cloud has a role.
41
+ */
42
+ id: 'context-map.q3-bbom-protected',
43
+ framework: 'ddd-context-map',
44
+ labelKey: 'com.labre.ddd-context-map.quality.bbom-protected',
45
+ fallback: 'Every downstream of a Big Ball of Mud is protected by an Anticorruption Layer.',
46
+ order: 3,
47
+ },
48
+ {
49
+ id: 'context-map.q4-legend',
50
+ framework: 'ddd-context-map',
51
+ labelKey: 'com.labre.ddd-context-map.quality.legend',
52
+ fallback: 'The map has a legend: a reader who does not know the notation can read it.',
53
+ order: 4,
54
+ },
55
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ValidationProfile } from '@formicoidea/labre-core/blocks/surface';
2
+ export declare const CONTEXT_MAP_PROFILES: readonly ValidationProfile[];
@@ -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 tooltipWording: import("@formicoidea/labre-core/shared/services").ChromeWording;
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 tooltipWording: import("@formicoidea/labre-core/shared/services").ChromeWording;
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;