@formicoidea/labre-framework-ddd-core-domain 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands.js CHANGED
@@ -8,10 +8,10 @@ import { markerRole, subdomainRole } from './roles.js';
8
8
  * The notation legend stays in the map-background context menu (toolbar
9
9
  * config) — the element toolbar is out of scope for `docs/adr/0008`.
10
10
  */
11
- const chartSwatch = svg `<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="3" width="17" height="17" fill="#4d9900" fill-opacity="0.5"/><rect x="4" y="3" width="6" height="17" fill="#9933ff" fill-opacity="0.5"/><path d="M4 20 V3 M4 20 H21" stroke="currentColor" stroke-width="1.8"/></svg>`;
12
- const dotSwatch = (color) => svg `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="${color}" stroke="#1f2328" stroke-width="1.2"/></svg>`;
13
- const movementSwatch = svg `<svg viewBox="0 0 24 24" fill="none"><path d="M3 18 L16 7" stroke="${MOVEMENT_COLOR}" stroke-width="2" stroke-dasharray="3 3"/><path d="M12 6 L18 5 L17 11" stroke="${MOVEMENT_COLOR}" stroke-width="2" fill="none"/></svg>`;
14
- const markerSwatch = (fill, letter) => svg `<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="3" fill="${fill}" stroke="#1f2328" stroke-width="1.2"/><text x="12" y="16" text-anchor="middle" font-size="11" font-family="sans-serif" fill="#1f2328">${letter}</text></svg>`;
11
+ const chartSwatch = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="4" y="3" width="17" height="17" fill="#4d9900" fill-opacity="0.5"/><rect x="4" y="3" width="6" height="17" fill="#9933ff" fill-opacity="0.5"/><path d="M4 20 V3 M4 20 H21" stroke="currentColor" stroke-width="1.8"/></svg>`;
12
+ const dotSwatch = (color) => svg `<svg width="24" height="24" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="${color}" stroke="#1f2328" stroke-width="1.2"/></svg>`;
13
+ const movementSwatch = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M3 18 L16 7" stroke="${MOVEMENT_COLOR}" stroke-width="2" stroke-dasharray="3 3"/><path d="M12 6 L18 5 L17 11" stroke="${MOVEMENT_COLOR}" stroke-width="2" fill="none"/></svg>`;
14
+ const markerSwatch = (fill, letter) => svg `<svg width="24" height="24" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="3" fill="${fill}" stroke="#1f2328" stroke-width="1.2"/><text x="12" y="16" text-anchor="middle" font-size="11" font-family="sans-serif" fill="#1f2328">${letter}</text></svg>`;
15
15
  const SPECS = [
16
16
  {
17
17
  id: 'addChart',
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
3
3
  export { coreDomainTranslationEntries } from './translations.js';
4
4
  export { CORE_DOMAIN_BACKGROUND, CORE_DOMAIN_LEGEND_TONES, } from './core-domain/background.js';
5
5
  export { coreDomain } from './core-domain/element-renderer.js';
6
+ export { CORE_DOMAIN_MORPH_FAMILIES, CORE_DOMAIN_MORPH_SPEC, coreDomainArtefactOf, coreDomainMorphClears, coreDomainMorphedCaption, coreDomainMorphProps, type CdMorphKind, } from './morph.js';
6
7
  export { CORE_DOMAIN_NUDGES } from './nudges.js';
7
8
  export { CORE_DOMAIN_PROFILES } from './profiles.js';
8
9
  export { CORE_DOMAIN_ROLE, CORE_DOMAIN_ROLES, type CoreDomainRole, type CoreDomainRoleId, subdomainRole, } from './roles.js';
package/dist/index.js CHANGED
@@ -3,6 +3,10 @@ export { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
3
3
  export { coreDomainTranslationEntries } from './translations.js';
4
4
  export { CORE_DOMAIN_BACKGROUND, CORE_DOMAIN_LEGEND_TONES, } from './core-domain/background.js';
5
5
  export { coreDomain } from './core-domain/element-renderer.js';
6
+ // The morph: the families the dropdown is parameterized by, the resolution from
7
+ // a selected group to the shape the role lives on, the patch a kind is worth,
8
+ // and the caption rule — all of them what the unit and integration suites drive.
9
+ export { CORE_DOMAIN_MORPH_FAMILIES, CORE_DOMAIN_MORPH_SPEC, coreDomainArtefactOf, coreDomainMorphClears, coreDomainMorphedCaption, coreDomainMorphProps, } from './morph.js';
6
10
  export { CORE_DOMAIN_NUDGES } from './nudges.js';
7
11
  export { CORE_DOMAIN_PROFILES } from './profiles.js';
8
12
  export { CORE_DOMAIN_ROLE, CORE_DOMAIN_ROLES, subdomainRole, } from './roles.js';
@@ -0,0 +1,117 @@
1
+ import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ShapeElementModel } from '@formicoidea/labre-core/model';
3
+ import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
4
+ import { type CdMarkerKind, type CdSubdomainKind } from './roles.js';
5
+ /**
6
+ * What a Core Domain artefact may BECOME — the declaration behind the "Change
7
+ * type" dropdown on a selected dot's or marker's contextual toolbar.
8
+ *
9
+ * ## Two families, and nothing between them
10
+ *
11
+ * A big bet and a platform sub-domain are the same artefact said differently:
12
+ * both are a dot plotted at a (differentiation, complexity) position, and
13
+ * realising halfway through a chart that this one is really a platform is
14
+ * modelling rather than a mistake. The two bounded contexts are in that family
15
+ * too — `roles.ts` already parents all five on `core-domain:subdomain`, because
16
+ * a bounded context is placed and read exactly like the other three and what
17
+ * makes it a `bc-current` is only that a movement may start from it.
18
+ *
19
+ * The three Team Topologies markers are the second family and the two never
20
+ * meet. That is the same disjunction `roles.ts` draws: a marker is an
21
+ * ANNOTATION about how work flows between teams, a sub-domain is a thing
22
+ * plotted on the chart, and no rule written on one may ever fall on the other.
23
+ * Turning a dot into a square would move an artefact from the picture to the
24
+ * commentary about it, which is not "the same thing, said more precisely". The
25
+ * generic module refuses a mixed selection on its own (the two kinds are in no
26
+ * common family), so this stays a statement of data rather than a check.
27
+ *
28
+ * The chart background and the movement edge are in NO family: the chart is the
29
+ * frame of reference the others are measured against, and the movement is an
30
+ * edge whose morph would be a different gesture entirely (`docs/adr/0010`).
31
+ */
32
+ export declare const CORE_DOMAIN_MORPH_FAMILIES: readonly (readonly CdMorphKind[])[];
33
+ /** Everything that morphs in this framework: the five dots and the three markers. */
34
+ export type CdMorphKind = CdSubdomainKind | CdMarkerKind;
35
+ /**
36
+ * What a kind is worth to a shape that ALREADY EXISTS — the creation preset
37
+ * minus the three things a morph has no business touching.
38
+ *
39
+ * ## Why the whole preset, and not `{role, fillColor}`
40
+ *
41
+ * Because it is insurance, and it is free. Every member of a family shares its
42
+ * stroke, its stroke width, its silhouette and — inside the markers — its corner
43
+ * radius today, so all but `role` and `fillColor` are inert on this table. That
44
+ * is exactly the state BPMN's families were in before `subProcess` /
45
+ * `callActivity` gained a border weight that mattered: a family is DATA and
46
+ * grows by declaration, with no code change to prompt anyone to check the
47
+ * presets. Derived from the creation builder, the palette and the morph cannot
48
+ * drift apart whatever is added, and a morphed dot is byte-for-byte a dot
49
+ * freshly placed from the sub-menu.
50
+ */
51
+ export declare function coreDomainMorphProps(kind: CdMorphKind): Record<string, unknown>;
52
+ /**
53
+ * The fields to DELETE after morphing to `kind` — the keys some other kind
54
+ * writes and this one does not.
55
+ *
56
+ * EMPTY for every kind today: `dddShapeProps` writes one fixed key set whatever
57
+ * the options say, and the label branch it does spread conditionally is never
58
+ * taken here (a dot's caption is a sibling text element, not the shape's own
59
+ * words). Derived rather than hard-coded to `[]` for the reason C4's equivalent
60
+ * is: a patch cannot express absence, and the day one preset stops writing a key
61
+ * the previous kind's value would otherwise stay in the Y.Map, silently in
62
+ * force.
63
+ */
64
+ export declare function coreDomainMorphClears(kind: CdMorphKind): readonly string[];
65
+ /**
66
+ * The dot or the marker square a selected GROUP is built round — `undefined`
67
+ * when the group is not one of ours.
68
+ *
69
+ * Both artefacts are composites: a dot is `group[ellipse, caption]` and a marker
70
+ * is `group[square, letter, caption]`, so what a click selects is the group and
71
+ * what carries the role is the shape inside it. This is the whole of the
72
+ * indirection and it is also the gate: a plain group somebody lassoed round
73
+ * three rectangles, a C4 component, a legend box and a group of two dots all
74
+ * answer `undefined`, so none of them is ever offered the menu.
75
+ *
76
+ * TWO artefacts is a refusal rather than a first-wins pick, for the reason
77
+ * `c4NodeOfComponent` gives: morphing "it" would mean choosing one of them by
78
+ * document order, and the honest answer to an ambiguous selection is nothing.
79
+ */
80
+ export declare function coreDomainArtefactOf(model: GfxPrimitiveElementModel): ShapeElementModel | undefined;
81
+ /**
82
+ * The caption an artefact should carry once it has morphed — or `null` when the
83
+ * words are the AUTHOR's and must not be touched.
84
+ *
85
+ * The C4 title rule, applied to the one caption these artefacts have: exactly
86
+ * one case rewrites, the caption being the source kind's own creation prompt
87
+ * letter for letter, which is what a dot nobody has named still says. Everything
88
+ * else — a name typed over it, the prompt with a word added, another kind's
89
+ * prompt, a cleared caption — is content, and content survives a morph
90
+ * untouched.
91
+ *
92
+ * It is the tier a user sees go wrong: every one of the eight kinds prompts with
93
+ * a different word, so a fresh "Big-bet sub-domain" morphed to a platform would
94
+ * otherwise be a blue dot captioned "Big-bet sub-domain" — a picture flatly
95
+ * contradicting its own name.
96
+ *
97
+ * Pure and total over every string, like `c4MorphedTitle`: the input is a canvas
98
+ * text element somebody may have typed anything into.
99
+ */
100
+ export declare function coreDomainMorphedCaption(from: CdMorphKind, to: CdMorphKind, rawText: string | null | undefined): string | null;
101
+ /**
102
+ * The Core Domain morph declaration, handed to the generic `morphToolbarConfig`.
103
+ *
104
+ * `modelType` is the native `GroupElementModel` and not `ShapeElementModel`,
105
+ * because both artefacts are composites and the group is what a click on one
106
+ * selects. Everything that makes that safe is in {@link coreDomainArtefactOf}:
107
+ * the toolbar's own homogeneity test only proves that every selected element is
108
+ * A group, and the resolution proves that every one of them is one of ours.
109
+ *
110
+ * Every shipped creation path builds the group — `commands.ts` always passes a
111
+ * caption, so `addDot`'s bare-ellipse branch is unreachable from the palette,
112
+ * the templates and the agent alike. A dot that somehow arrives ungrouped is
113
+ * simply not offered the menu, which is the same refusal a role-less dot gets
114
+ * and for the same reason: nothing is inferred, nothing is repaired behind the
115
+ * user's back.
116
+ */
117
+ export declare const CORE_DOMAIN_MORPH_SPEC: MorphSpec<CdMorphKind>;
package/dist/morph.js ADDED
@@ -0,0 +1,344 @@
1
+ import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { CD_SUBDOMAINS, dddShapeProps, dotShapeOpts, markerShapeOpts, TEAM_TOPOLOGIES, } from '@formicoidea/labre-ddd-shared';
3
+ import { GroupElementModel, ShapeElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
4
+ import { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
5
+ import { CORE_DOMAIN_ROLE, markerRole, subdomainRole, } from './roles.js';
6
+ /**
7
+ * What a Core Domain artefact may BECOME — the declaration behind the "Change
8
+ * type" dropdown on a selected dot's or marker's contextual toolbar.
9
+ *
10
+ * ## Two families, and nothing between them
11
+ *
12
+ * A big bet and a platform sub-domain are the same artefact said differently:
13
+ * both are a dot plotted at a (differentiation, complexity) position, and
14
+ * realising halfway through a chart that this one is really a platform is
15
+ * modelling rather than a mistake. The two bounded contexts are in that family
16
+ * too — `roles.ts` already parents all five on `core-domain:subdomain`, because
17
+ * a bounded context is placed and read exactly like the other three and what
18
+ * makes it a `bc-current` is only that a movement may start from it.
19
+ *
20
+ * The three Team Topologies markers are the second family and the two never
21
+ * meet. That is the same disjunction `roles.ts` draws: a marker is an
22
+ * ANNOTATION about how work flows between teams, a sub-domain is a thing
23
+ * plotted on the chart, and no rule written on one may ever fall on the other.
24
+ * Turning a dot into a square would move an artefact from the picture to the
25
+ * commentary about it, which is not "the same thing, said more precisely". The
26
+ * generic module refuses a mixed selection on its own (the two kinds are in no
27
+ * common family), so this stays a statement of data rather than a check.
28
+ *
29
+ * The chart background and the movement edge are in NO family: the chart is the
30
+ * frame of reference the others are measured against, and the movement is an
31
+ * edge whose morph would be a different gesture entirely (`docs/adr/0010`).
32
+ */
33
+ export const CORE_DOMAIN_MORPH_FAMILIES = [
34
+ // Derived from the shared preset tables rather than restated, so declaration
35
+ // order is MENU order and the menu reads in the order the senior sub-menu
36
+ // offers them — a sixth dot preset arrives in the dropdown with no edit here.
37
+ CD_SUBDOMAINS.map(preset => preset.kind),
38
+ TEAM_TOPOLOGIES.map(preset => preset.kind),
39
+ ];
40
+ /** A kind's own role — the id the patch writes and the telemetry reports. */
41
+ function roleOf(kind) {
42
+ return CORE_DOMAIN_ROLE[kind];
43
+ }
44
+ /**
45
+ * Role → kind, the inverse of {@link CORE_DOMAIN_ROLE} restricted to the eight
46
+ * morphable kinds.
47
+ *
48
+ * The kind of a Core Domain artefact has nowhere else to live: a dot is a
49
+ * native `shape` and a marker is a native `shape`, so unlike a `bpmnNode` or a
50
+ * `c4Node` there is no `kind` field to read — the role IS the kind, which is the
51
+ * arrangement `commands.ts` has stamped since the roles landed.
52
+ *
53
+ * An element carrying no role is therefore an element with no kind, and it is
54
+ * REFUSED rather than guessed at. A chart drawn before the vocabulary existed
55
+ * carries role-less dots, and the promise `roles.ts` makes about them is that
56
+ * nothing infers anything from their colour: no backfill, ever (#71). Such a dot
57
+ * opens, paints and round-trips exactly as it always did; it just is not offered
58
+ * a menu that would have to invent what it is first.
59
+ *
60
+ * Null prototype, like the vocabulary itself: this is a lookup keyed by strings
61
+ * read off a document, so `KIND_OF_ROLE['toString']` must not resolve.
62
+ */
63
+ const KIND_OF_ROLE = Object.assign(Object.create(null), Object.fromEntries(CORE_DOMAIN_MORPH_FAMILIES.flat().map(kind => [roleOf(kind), kind])));
64
+ /** Whether a kind is one of the three markers — the family with a glyph. */
65
+ function isMarkerKind(kind) {
66
+ return kind in MARKER_LETTER;
67
+ }
68
+ /* ── The patch one kind is worth ───────────────────────────────────────── */
69
+ /**
70
+ * The box {@link coreDomainMorphProps} hands the shared builder and then throws
71
+ * away. Never written to a document: a morph keeps the geometry the element
72
+ * already has, and this exists only because the one builder takes a box.
73
+ */
74
+ const DISCARDED_BOX = [0, 0, 0, 0];
75
+ /** What a morph must never rewrite: identity, geometry, and the user's words. */
76
+ const NOT_A_MORPH = ['type', 'xywh', 'text'];
77
+ /**
78
+ * What each kind IS, as shape options — the very objects `addDot` and
79
+ * `addMarker` build their artefact from ({@link dotShapeOpts},
80
+ * {@link markerShapeOpts}).
81
+ */
82
+ const SHAPE_OPTS = Object.assign(Object.create(null), Object.fromEntries([
83
+ ...CD_SUBDOMAINS.map(preset => [
84
+ preset.kind,
85
+ dotShapeOpts(preset.fill, subdomainRole(preset.kind)),
86
+ ]),
87
+ ...TEAM_TOPOLOGIES.map(preset => [
88
+ preset.kind,
89
+ markerShapeOpts(preset.fill, markerRole(preset.kind)),
90
+ ]),
91
+ ]));
92
+ /**
93
+ * What a kind is worth to a shape that ALREADY EXISTS — the creation preset
94
+ * minus the three things a morph has no business touching.
95
+ *
96
+ * ## Why the whole preset, and not `{role, fillColor}`
97
+ *
98
+ * Because it is insurance, and it is free. Every member of a family shares its
99
+ * stroke, its stroke width, its silhouette and — inside the markers — its corner
100
+ * radius today, so all but `role` and `fillColor` are inert on this table. That
101
+ * is exactly the state BPMN's families were in before `subProcess` /
102
+ * `callActivity` gained a border weight that mattered: a family is DATA and
103
+ * grows by declaration, with no code change to prompt anyone to check the
104
+ * presets. Derived from the creation builder, the palette and the morph cannot
105
+ * drift apart whatever is added, and a morphed dot is byte-for-byte a dot
106
+ * freshly placed from the sub-menu.
107
+ */
108
+ export function coreDomainMorphProps(kind) {
109
+ const props = dddShapeProps(...DISCARDED_BOX, SHAPE_OPTS[kind]);
110
+ for (const key of NOT_A_MORPH)
111
+ delete props[key];
112
+ return props;
113
+ }
114
+ /**
115
+ * Every key ANY kind's props may carry — the union over both families.
116
+ *
117
+ * Computed rather than listed, so a preset that starts spreading a key
118
+ * conditionally is covered on the day it is added rather than on the day
119
+ * somebody notices.
120
+ */
121
+ const EVERY_MORPH_KEY = new Set(CORE_DOMAIN_MORPH_FAMILIES.flat().flatMap(kind => Object.keys(coreDomainMorphProps(kind))));
122
+ /**
123
+ * The fields to DELETE after morphing to `kind` — the keys some other kind
124
+ * writes and this one does not.
125
+ *
126
+ * EMPTY for every kind today: `dddShapeProps` writes one fixed key set whatever
127
+ * the options say, and the label branch it does spread conditionally is never
128
+ * taken here (a dot's caption is a sibling text element, not the shape's own
129
+ * words). Derived rather than hard-coded to `[]` for the reason C4's equivalent
130
+ * is: a patch cannot express absence, and the day one preset stops writing a key
131
+ * the previous kind's value would otherwise stay in the Y.Map, silently in
132
+ * force.
133
+ */
134
+ export function coreDomainMorphClears(kind) {
135
+ const present = new Set(Object.keys(coreDomainMorphProps(kind)));
136
+ return [...EVERY_MORPH_KEY].filter(key => !present.has(key));
137
+ }
138
+ /* ── Resolving the composite ───────────────────────────────────────────── */
139
+ /**
140
+ * The dot or the marker square a selected GROUP is built round — `undefined`
141
+ * when the group is not one of ours.
142
+ *
143
+ * Both artefacts are composites: a dot is `group[ellipse, caption]` and a marker
144
+ * is `group[square, letter, caption]`, so what a click selects is the group and
145
+ * what carries the role is the shape inside it. This is the whole of the
146
+ * indirection and it is also the gate: a plain group somebody lassoed round
147
+ * three rectangles, a C4 component, a legend box and a group of two dots all
148
+ * answer `undefined`, so none of them is ever offered the menu.
149
+ *
150
+ * TWO artefacts is a refusal rather than a first-wins pick, for the reason
151
+ * `c4NodeOfComponent` gives: morphing "it" would mean choosing one of them by
152
+ * document order, and the honest answer to an ambiguous selection is nothing.
153
+ */
154
+ export function coreDomainArtefactOf(model) {
155
+ if (!(model instanceof GroupElementModel))
156
+ return undefined;
157
+ let found;
158
+ for (const child of model.childElements) {
159
+ if (!(child instanceof ShapeElementModel))
160
+ continue;
161
+ if (kindOfArtefact(child) === undefined)
162
+ continue;
163
+ if (found)
164
+ return undefined;
165
+ found = child;
166
+ }
167
+ return found;
168
+ }
169
+ /** The kind a shape carries, read off its role. */
170
+ function kindOfArtefact(model) {
171
+ if (!(model instanceof ShapeElementModel))
172
+ return undefined;
173
+ return model.role ? KIND_OF_ROLE[model.role] : undefined;
174
+ }
175
+ /* ── The words the composite owes the change ───────────────────────────── */
176
+ /** A kind's creation caption, both families in one table. */
177
+ const KIND_LABEL = Object.fromEntries([
178
+ ...CD_SUBDOMAINS.map(preset => [preset.kind, preset.label]),
179
+ ...TEAM_TOPOLOGIES.map(preset => [preset.kind, preset.label]),
180
+ ]);
181
+ /** The glyph each marker wears — C, X, F. */
182
+ const MARKER_LETTER = Object.fromEntries(TEAM_TOPOLOGIES.map(preset => [preset.kind, preset.letter]));
183
+ /**
184
+ * The caption an artefact should carry once it has morphed — or `null` when the
185
+ * words are the AUTHOR's and must not be touched.
186
+ *
187
+ * The C4 title rule, applied to the one caption these artefacts have: exactly
188
+ * one case rewrites, the caption being the source kind's own creation prompt
189
+ * letter for letter, which is what a dot nobody has named still says. Everything
190
+ * else — a name typed over it, the prompt with a word added, another kind's
191
+ * prompt, a cleared caption — is content, and content survives a morph
192
+ * untouched.
193
+ *
194
+ * It is the tier a user sees go wrong: every one of the eight kinds prompts with
195
+ * a different word, so a fresh "Big-bet sub-domain" morphed to a platform would
196
+ * otherwise be a blue dot captioned "Big-bet sub-domain" — a picture flatly
197
+ * contradicting its own name.
198
+ *
199
+ * Pure and total over every string, like `c4MorphedTitle`: the input is a canvas
200
+ * text element somebody may have typed anything into.
201
+ */
202
+ export function coreDomainMorphedCaption(from, to, rawText) {
203
+ return (rawText ?? '').trim() === KIND_LABEL[from] ? KIND_LABEL[to] : null;
204
+ }
205
+ /**
206
+ * The two texts of a composite, told apart by WHERE they sit.
207
+ *
208
+ * The letter is centred ON the square and the caption stands beside the
209
+ * artefact, which is the notation's own arrangement rather than a detail of the
210
+ * builder: geometry survives a restyle, a re-word and a font change, where
211
+ * `textAlign`, a font size or "the text is one character long" would each stop
212
+ * being true the moment a user edits the thing they describe. A dot has no glyph
213
+ * at all and simply answers `undefined` for it.
214
+ *
215
+ * Either half is taken only when it is the ONE candidate. A group somebody has
216
+ * added a second caption to is a group where "the caption" is a guess, and this
217
+ * would rather leave both alone than rewrite the wrong one.
218
+ */
219
+ function wordsOf(group, artefact) {
220
+ const texts = group.childElements.filter((child) => child instanceof TextElementModel);
221
+ const on = (text) => {
222
+ const cx = text.x + text.w / 2;
223
+ const cy = text.y + text.h / 2;
224
+ return (cx >= artefact.x &&
225
+ cx <= artefact.x + artefact.w &&
226
+ cy >= artefact.y &&
227
+ cy <= artefact.y + artefact.h);
228
+ };
229
+ const only = (found) => found.length === 1 ? found[0] : undefined;
230
+ return {
231
+ glyph: only(texts.filter(on)),
232
+ caption: only(texts.filter(text => !on(text))),
233
+ };
234
+ }
235
+ /**
236
+ * Write one text, in place, if the decision says to.
237
+ *
238
+ * `rewriteTier`'s contract, and its reasons: the comparison is against the
239
+ * TRIMMED text — the same string `decide` was given — so padding is never
240
+ * rewritten away for nothing, and the `Y.Text` instance is mutated rather than
241
+ * replaced because it is what any bound editor holds. Inside the caller's
242
+ * `captureSync`, so it is part of the same single ctrl+z as the role that made
243
+ * it necessary.
244
+ */
245
+ function rewriteText(text, decide) {
246
+ if (!text || text.isLocked())
247
+ return;
248
+ const current = text.text.toString().trim();
249
+ const next = decide(current);
250
+ if (next === null || next === current)
251
+ return;
252
+ text.surface.store.transact(() => {
253
+ text.text.delete(0, text.text.length);
254
+ text.text.insert(0, next);
255
+ });
256
+ }
257
+ /**
258
+ * Keep the artefact's own words saying what it now is — the `afterMorph` half of
259
+ * the spec.
260
+ *
261
+ * Two texts and two DIFFERENT rules, because they are two different kinds of
262
+ * thing:
263
+ *
264
+ * - **The caption** is content, or a prompt standing in for content. Timid:
265
+ * placeholder→placeholder and nothing else ({@link coreDomainMorphedCaption}).
266
+ * A dot somebody called "Billing" is called that whatever it becomes.
267
+ * - **The letter** is NOTATION. C, X and F are not words anybody wrote, they are
268
+ * Team Topologies' own glyphs for the three interaction modes, and a square
269
+ * that has become an X-as-a-Service while still showing a C is not "a label
270
+ * the author owns" — it is the picture lying about itself. So it is rewritten
271
+ * ALWAYS, whatever it currently says, which is the only rule that also
272
+ * repairs a glyph a previous edit left wrong.
273
+ */
274
+ function rewriteWords(model, from, to) {
275
+ if (!(model instanceof GroupElementModel))
276
+ return;
277
+ const artefact = coreDomainArtefactOf(model);
278
+ if (!artefact)
279
+ return;
280
+ const { glyph, caption } = wordsOf(model, artefact);
281
+ rewriteText(caption, text => coreDomainMorphedCaption(from, to, text));
282
+ if (isMarkerKind(to))
283
+ rewriteText(glyph, () => MARKER_LETTER[to]);
284
+ }
285
+ /* ── The menu's own wording ────────────────────────────────────────────── */
286
+ /**
287
+ * The creation command that places each kind, keyed BY that kind.
288
+ *
289
+ * Derived from `telemetry.element`, which is where the kind is already written
290
+ * down (`subdomain:platform`, `team-topology:xaas`) and is documented as a
291
+ * historical value that must not be renamed. Deriving rather than restating is
292
+ * what stops a second table of labels and icons drifting from the one the
293
+ * sub-menu and the catalogue read.
294
+ */
295
+ const ARTEFACT_PREFIXES = ['subdomain:', 'team-topology:'];
296
+ const ARTEFACT_COMMANDS = new Map(coreDomainCommands.flatMap(command => {
297
+ const element = command.telemetry?.element ?? '';
298
+ const prefix = ARTEFACT_PREFIXES.find(candidate => element.startsWith(candidate));
299
+ return prefix ? [[element.slice(prefix.length), command]] : [];
300
+ }));
301
+ /** A kind's wording: the creation command's own key and English. */
302
+ function labelOf(kind) {
303
+ const command = ARTEFACT_COMMANDS.get(kind);
304
+ return {
305
+ key: command?.labelKey,
306
+ fallback: command?.labelFallback ?? KIND_LABEL[kind],
307
+ };
308
+ }
309
+ /** A kind's icon: the creation command's own swatch, reused rather than redrawn. */
310
+ function iconOf(kind) {
311
+ const iconKey = ARTEFACT_COMMANDS.get(kind)?.iconKey;
312
+ return ((iconKey && coreDomainCommandIcons[iconKey]) ||
313
+ coreDomainCommandIcons['ddd-core-domain.chart']);
314
+ }
315
+ /**
316
+ * The Core Domain morph declaration, handed to the generic `morphToolbarConfig`.
317
+ *
318
+ * `modelType` is the native `GroupElementModel` and not `ShapeElementModel`,
319
+ * because both artefacts are composites and the group is what a click on one
320
+ * selects. Everything that makes that safe is in {@link coreDomainArtefactOf}:
321
+ * the toolbar's own homogeneity test only proves that every selected element is
322
+ * A group, and the resolution proves that every one of them is one of ours.
323
+ *
324
+ * Every shipped creation path builds the group — `commands.ts` always passes a
325
+ * caption, so `addDot`'s bare-ellipse branch is unreachable from the palette,
326
+ * the templates and the agent alike. A dot that somehow arrives ungrouped is
327
+ * simply not offered the menu, which is the same refusal a role-less dot gets
328
+ * and for the same reason: nothing is inferred, nothing is repaired behind the
329
+ * user's back.
330
+ */
331
+ export const CORE_DOMAIN_MORPH_SPEC = {
332
+ framework: 'ddd-core-domain',
333
+ families: CORE_DOMAIN_MORPH_FAMILIES,
334
+ modelType: GroupElementModel,
335
+ resolveTarget: coreDomainArtefactOf,
336
+ kindOf: kindOfArtefact,
337
+ roleOf,
338
+ propsOf: coreDomainMorphProps,
339
+ clearOf: coreDomainMorphClears,
340
+ afterMorph: rewriteWords,
341
+ labelOf,
342
+ iconOf,
343
+ label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
344
+ };
package/dist/view.js CHANGED
@@ -1,6 +1,6 @@
1
- import { QualityNudgeExtension, validationToolbarConfig, ValidationProfileExtension, ValidationRuleExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { morphToolbarConfig, QualityNudgeExtension, validationToolbarConfig, ValidationProfileExtension, ValidationRuleExtension, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ViewExtensionProvider, } from '@formicoidea/labre-core/ext-loader';
3
- import { ToolbarModuleExtension } from '@formicoidea/labre-core/shared/services';
3
+ import { ToolbarModuleExtension, toolbarModuleKey, } from '@formicoidea/labre-core/shared/services';
4
4
  import { BlockFlavourIdentifier, CommandExtension } from '@formicoidea/labre-core/std';
5
5
  import { RoleVocabularyExtension } from '@formicoidea/labre-core/std/gfx';
6
6
  import { coreDomainCommandIcons, coreDomainCommands } from './commands.js';
@@ -8,6 +8,7 @@ import { CoreDomainRendererExtension } from './core-domain/element-renderer.js';
8
8
  import { CoreDomainInteraction, CoreDomainView, } from './core-domain/element-view.js';
9
9
  import { coreDomainToolbarExtension } from './core-domain/toolbar-config.js';
10
10
  import { coreDomainEffects } from './effects.js';
11
+ import { CORE_DOMAIN_MORPH_SPEC } from './morph.js';
11
12
  import { CORE_DOMAIN_NUDGES } from './nudges.js';
12
13
  import { CORE_DOMAIN_PROFILES } from './profiles.js';
13
14
  import { CORE_DOMAIN_ROLES } from './roles.js';
@@ -79,6 +80,33 @@ export class DddCoreDomainViewExtension extends ViewExtensionProvider {
79
80
  }));
80
81
  context.register(coreDomainSeniorTool);
81
82
  context.register(CommandExtension(coreDomainCommands, coreDomainCommandIcons));
83
+ // The "Change type" dropdown on a selected dot's or marker's contextual
84
+ // toolbar — the generic module, parameterized by this framework's two
85
+ // families.
86
+ //
87
+ // ## Why the key carries an owner
88
+ //
89
+ // Both artefacts are native `group`s, so the row the toolbar draws for one
90
+ // is the GROUP's row, merged from `affine:surface:group`,
91
+ // `custom:affine:surface:group` and the surface wildcards. Both group keys
92
+ // are long since claimed — the native group operations on one, Wardley's
93
+ // qualification dropdown on the other, with C4's morph already registered
94
+ // under a suffixed variant — and a second module on either would throw
95
+ // `DuplicateServiceDefinitionError` before the editor finished setting up.
96
+ // `toolbarModuleKey` is what lifts that ceiling: the module is registered
97
+ // under `custom:affine:surface:group#ddd-core-domain-morph` and the
98
+ // registry hands it to the same row.
99
+ //
100
+ // ## Why here
101
+ //
102
+ // In the flag-gated half, because a morph is TOOLING: a dot placed while
103
+ // the flag was on keeps its role, its colour, its caption and its place in
104
+ // every rule when the flag goes off — it just stops being something the
105
+ // toolbar offers to say differently (`docs/adr/0009`).
106
+ context.register(ToolbarModuleExtension({
107
+ id: BlockFlavourIdentifier(toolbarModuleKey('custom:affine:surface:group', 'ddd-core-domain-morph')),
108
+ config: morphToolbarConfig(CORE_DOMAIN_MORPH_SPEC),
109
+ }));
82
110
  }
83
111
  }
84
112
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-ddd-core-domain",
3
3
  "description": "Labre ddd-core-domain framework for @formicoidea/labre-core.",
4
- "version": "0.34.1",
4
+ "version": "0.35.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -31,8 +31,8 @@
31
31
  "dist"
32
32
  ],
33
33
  "dependencies": {
34
- "@formicoidea/labre-core": "0.34.1",
35
- "@formicoidea/labre-ddd-shared": "0.34.1",
34
+ "@formicoidea/labre-core": "0.35.0",
35
+ "@formicoidea/labre-ddd-shared": "0.35.0",
36
36
  "lit": "^3.2.0"
37
37
  }
38
38
  }