@formicoidea/labre-framework-wardley 0.34.2 → 0.36.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
@@ -1,7 +1,7 @@
1
1
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
2
- import { activateWardleyConnector, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
2
+ import { activateWardleyConnector, createWardleyAccelerator, createWardleyArea, createWardleyBackground, createWardleyInertia, createWardleyMarket, createWardleyNode, createWardleyPipeline, createWardleyPorter, exportOwmFile, importOwmFile, importWardleySvgFile, wardleyMapsOnBoard, } from './actions.js';
3
3
  import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
4
- import { wardleyAnchorIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, } from './toolbar/icons.js';
4
+ import { wardleyAcceleratorIcon, wardleyAnchorIcon, wardleyAreaPolygonIcon, wardleyAreaRectIcon, wardleyArrowIcon, wardleyBackgroundIcon, wardleyDeceleratorIcon, wardleyBenefitIcon, wardleyComponentIcon, wardleyEcosystemIcon, wardleyEvolutionGradientIcon, wardleyExportOwmIcon, wardleyImportOwmIcon, wardleyImportSvgIcon, wardleyInertiaIcon, wardleyLinkIcon, wardleyMarketIcon, wardleyMethodIcon, wardleyOpportunityIcon, wardleyPipelineIcon, wardleyPorterIcon, } from './toolbar/icons.js';
5
5
  const SPECS = [
6
6
  {
7
7
  id: 'addBackground',
@@ -12,6 +12,7 @@ const SPECS = [
12
12
  category: 'backgrounds',
13
13
  kind: 'artefact',
14
14
  element: 'background:classic',
15
+ board: true,
15
16
  run: gfx => createWardleyBackground(gfx, 'classic'),
16
17
  },
17
18
  {
@@ -21,6 +22,7 @@ const SPECS = [
21
22
  category: 'backgrounds',
22
23
  kind: 'artefact',
23
24
  element: 'background:opportunity',
25
+ board: true,
24
26
  run: gfx => createWardleyBackground(gfx, 'opportunity'),
25
27
  },
26
28
  {
@@ -30,6 +32,7 @@ const SPECS = [
30
32
  category: 'backgrounds',
31
33
  kind: 'artefact',
32
34
  element: 'background:benefit',
35
+ board: true,
33
36
  run: gfx => createWardleyBackground(gfx, 'benefit'),
34
37
  },
35
38
  {
@@ -39,6 +42,7 @@ const SPECS = [
39
42
  category: 'backgrounds',
40
43
  kind: 'artefact',
41
44
  element: 'background:evolution-gradient',
45
+ board: true,
42
46
  run: gfx => createWardleyBackground(gfx, 'evolution-gradient'),
43
47
  },
44
48
  {
@@ -137,6 +141,69 @@ const SPECS = [
137
141
  element: 'node:inertia',
138
142
  run: createWardleyInertia,
139
143
  },
144
+ // Last of the nodes, and last of the row: a Porter's force is the only
145
+ // artefact on this palette that is not part of the map — it is what presses
146
+ // on it from outside — so it reads as an annotation you reach for once the
147
+ // chain is drawn. Keyless: the `w` chord already seats eight artefacts, and
148
+ // a framework binds past that by host override rather than by default.
149
+ {
150
+ id: 'addPorter',
151
+ label: "Porter's forces",
152
+ iconKey: 'wardley.porter',
153
+ category: 'nodes',
154
+ kind: 'artefact',
155
+ element: 'node:porter',
156
+ run: createWardleyPorter,
157
+ },
158
+ // The two climate annotations, after the force and for the same reason: they
159
+ // are not links in the value chain either, and they are reached for once the
160
+ // chain is drawn and the question becomes how fast it is moving. Keyless,
161
+ // like the porter — the `w` chord seats eight artefacts and a framework binds
162
+ // past that by host override.
163
+ {
164
+ id: 'addAccelerator',
165
+ label: 'Accelerator',
166
+ iconKey: 'wardley.accelerator',
167
+ category: 'nodes',
168
+ kind: 'artefact',
169
+ element: 'node:accelerator',
170
+ run: gfx => createWardleyAccelerator(gfx, 'accelerator'),
171
+ },
172
+ {
173
+ id: 'addDecelerator',
174
+ label: 'Decelerator',
175
+ iconKey: 'wardley.decelerator',
176
+ category: 'nodes',
177
+ kind: 'artefact',
178
+ element: 'node:decelerator',
179
+ run: gfx => createWardleyAccelerator(gfx, 'decelerator'),
180
+ },
181
+ // The zones, last of the toolbox and after the connectors: an area is drawn
182
+ // around a chain that already exists, so it is the last gesture of a map and
183
+ // reads as the last entry of the row. Two entries and ONE kind — the shape is
184
+ // the choice, and offering it as two buttons is what saves the author a trip
185
+ // through "switch type" for the commonest case.
186
+ //
187
+ // Keyless, like the porter and the two climate arrows: the `w` chord seats
188
+ // eight artefacts and a framework binds past that by host override.
189
+ {
190
+ id: 'addAreaRect',
191
+ label: 'Area (rectangle)',
192
+ iconKey: 'wardley.area-rect',
193
+ category: 'areas',
194
+ kind: 'artefact',
195
+ element: 'node:area-rect',
196
+ run: gfx => createWardleyArea(gfx, 'rect'),
197
+ },
198
+ {
199
+ id: 'addAreaPolygon',
200
+ label: 'Area (polygon)',
201
+ iconKey: 'wardley.area-polygon',
202
+ category: 'areas',
203
+ kind: 'artefact',
204
+ element: 'node:area-polygon',
205
+ run: gfx => createWardleyArea(gfx, 'polygon'),
206
+ },
140
207
  ];
141
208
  /**
142
209
  * The sentence a tool shows under its label — the role's own gesture hint
@@ -171,7 +238,7 @@ const toolboxCommands = SPECS.map((spec, order) => ({
171
238
  : { mac: [], other: [] },
172
239
  availability: 'always',
173
240
  run: std => spec.run(std.get(GfxControllerIdentifier)),
174
- telemetry: { framework: 'wardley', element: spec.element },
241
+ telemetry: { framework: 'wardley', element: spec.element, board: spec.board },
175
242
  }));
176
243
  /**
177
244
  * The IMPORT — the first Wardley command whose subject is a whole map rather
@@ -183,9 +250,9 @@ const toolboxCommands = SPECS.map((spec, order) => ({
183
250
  * the sub-menu is the first thing a user opens, and "start from the map
184
251
  * somebody sent me" belongs in that row beside "start from a component" —
185
252
  * asking them to find the catalogue sidepanel first is the friction the
186
- * decision names. It is Wardley's fourteenth nomination, which is exactly the
187
- * cap, so nothing about the sub-menu's arbitration changes and no button is
188
- * pushed out of the row.
253
+ * decision names. It is the `+1` of ADR 0014's R4 — the single deliberate
254
+ * over-nomination the PO signed off on, and the reason this framework's
255
+ * nomination budget is `SENIOR_MENU_CAP + 1` rather than the cap itself.
189
256
  *
190
257
  * No `'contextual-toolbar'`: a contextual toolbar is a statement about a
191
258
  * SELECTION, and the moment this command is most wanted is on a board with
@@ -323,8 +390,25 @@ const importSvgCommand = {
323
390
  telemetry: { framework: 'wardley', element: 'board:import-svg' },
324
391
  };
325
392
  /**
326
- * The Wardley registry: the thirteen toolbox entries, then the two directions
393
+ * The Wardley registry: the eighteen toolbox entries, then the two directions
327
394
  * of the OWM DSL and the SVG fallback (`docs/adr/0012`).
395
+ *
396
+ * ## Nineteen nominations, and the curation decision behind them
397
+ *
398
+ * `addPorter` spent the last seat of ADR 0014 R4's `SENIOR_MENU_CAP + 1`
399
+ * budget, and `addAccelerator` / `addDecelerator` are the two past it. The PO
400
+ * made the call on 2026-09-03 (Amendment 2026-09-03 under R4): **every Wardley
401
+ * artefact nominates the row**. The row itself is unchanged — thirteen
402
+ * arbitrated seats plus "More artefacts…", the cap of fourteen it has had since
403
+ * the OWM pair tipped this framework over — so what the decision buys is that
404
+ * a newly-drawn artefact is ELIGIBLE for a seat, and recency and frequency
405
+ * decide who gets one. Nothing becomes unreachable: the catalogue lists
406
+ * everything, which is the registry's own invariant.
407
+ *
408
+ * `addAreaRect` and `addAreaPolygon` are the two after those, and they land
409
+ * under that same amendment rather than reopening it: they nominate like every
410
+ * other Wardley artefact, the row is still thirteen arbitrated seats plus "More
411
+ * artefacts…", and the catalogue grows by a section of its own.
328
412
  */
329
413
  export const wardleyCommands = [
330
414
  ...toolboxCommands,
@@ -347,6 +431,11 @@ export const wardleyCommandIcons = {
347
431
  'wardley.link': wardleyLinkIcon,
348
432
  'wardley.arrow': wardleyArrowIcon,
349
433
  'wardley.inertia': wardleyInertiaIcon,
434
+ 'wardley.porter': wardleyPorterIcon,
435
+ 'wardley.accelerator': wardleyAcceleratorIcon,
436
+ 'wardley.decelerator': wardleyDeceleratorIcon,
437
+ 'wardley.area-rect': wardleyAreaRectIcon,
438
+ 'wardley.area-polygon': wardleyAreaPolygonIcon,
350
439
  'wardley.import-owm': wardleyImportOwmIcon,
351
440
  'wardley.export-owm': wardleyExportOwmIcon,
352
441
  'wardley.import-svg': wardleyImportSvgIcon,
package/dist/export.js CHANGED
@@ -336,6 +336,10 @@ function labelAnchors(node, label) {
336
336
  *
337
337
  * A pipeline HANDLE never claims a label: it sits on the body's top edge, right
338
338
  * under the name the body owns, and would win every pipeline's name from it.
339
+ * A Porter's-forces glyph never claims one either, and for a sharper reason: it
340
+ * HAS no name — its one letter is the shape's own inner text — so any label it
341
+ * matched would be some neighbouring artefact's, taken out of that artefact's
342
+ * mouth and then thrown away, since a force is not written to the file at all.
339
343
  */
340
344
  function matchLabels(nodes, labels) {
341
345
  const pairs = [];
@@ -343,8 +347,15 @@ function matchLabels(nodes, labels) {
343
347
  // A handle sits on the pipeline body's top edge, directly under the name
344
348
  // the BODY owns, and would win it from the body every time. A roleless node
345
349
  // is a glyph's own wiring (a market's three inner dots) and names nothing.
346
- if (node.kind === 'handle' || node.role === undefined)
350
+ // An AREA claims none either, and it is the one that would do real damage:
351
+ // a zone is drawn around the components it groups, so its box sits next to
352
+ // every label on the map and would win one from the artefact it belongs to.
353
+ if (node.kind === 'handle' ||
354
+ node.kind === 'porter' ||
355
+ node.kind === 'area' ||
356
+ node.role === undefined) {
347
357
  return;
358
+ }
348
359
  const nodeBox = boxOf(node);
349
360
  labels.forEach((label, labelIndex) => {
350
361
  const labelBox = boxOf(label);
@@ -380,7 +391,17 @@ export function textOf(element) {
380
391
  * method is the `(build)` / `(buy)` / `(outsource)` decorator on a component
381
392
  * line — so a Labre method node is written as a component and the export warns
382
393
  * that the method itself could not be said. `pipeline` and `handle` are absent
383
- * because a pipeline is written from its BODY, in its own section.
394
+ * because a pipeline is written from its BODY, in its own section — and
395
+ * `porter` because OWM has no word for an external competition force at all —
396
+ * and `area` for the same silence, since the format has no word for a zone of
397
+ * the map either. The node loop skips both before this table is consulted, so a
398
+ * kind's absence here is never the reason it is left out.
399
+ *
400
+ * The climate annotations DO have a word, and OWM's own spelling of the second
401
+ * one is `deaccelerator` — not a typo on this side but the keyword the
402
+ * reference parser claims, so writing `decelerator` would produce a line no
403
+ * other Wardley tool reads. Both are written like a component line, `keyword
404
+ * name [visibility, evolution]`, which is exactly what they are in that format.
384
405
  */
385
406
  const OWM_KEYWORD_OF_KIND = {
386
407
  component: 'component',
@@ -388,6 +409,8 @@ const OWM_KEYWORD_OF_KIND = {
388
409
  market: 'market',
389
410
  ecosystem: 'ecosystem',
390
411
  method: 'component',
412
+ accelerator: 'accelerator',
413
+ decelerator: 'deaccelerator',
391
414
  };
392
415
  /** What one element carried from the file it came out of, if anything. */
393
416
  function carriedOf(element) {
@@ -506,11 +529,30 @@ export function exportWardleyOwmWithWarnings(board, options = {}) {
506
529
  const nodeLines = [];
507
530
  const pipelineLines = [];
508
531
  const methodNodes = [];
532
+ const porterNodes = [];
533
+ const areaNodes = [];
509
534
  for (const node of board.nodes) {
510
535
  if (node.role === undefined)
511
536
  continue; // a glyph's own wiring, not an artefact
512
537
  if (node.kind === 'handle')
513
538
  continue;
539
+ // Skipped BEFORE `nameOf`, and that is the whole reason it is a branch of
540
+ // its own rather than a missing entry in `OWM_KEYWORD_OF_KIND`: a force
541
+ // carries no name, so asking for one would christen it "Component 3" and
542
+ // count it among the artefacts that lost theirs — a warning about a loss
543
+ // that never happened, on a line the file does not contain.
544
+ if (node.kind === 'porter') {
545
+ porterNodes.push(node.id);
546
+ continue;
547
+ }
548
+ // Same branch, same reason: an area's name is its own inner text rather
549
+ // than a label beside it, so `nameOf` would find none and christen the zone
550
+ // "Component 4" — counting a nameless artefact among the ones that lost
551
+ // their name, on a line the file does not contain either way.
552
+ if (node.kind === 'area') {
553
+ areaNodes.push(node.id);
554
+ continue;
555
+ }
514
556
  if (twins.has(node.id))
515
557
  continue; // written by its `evolve` line
516
558
  const name = nameOf(node);
@@ -597,6 +639,12 @@ export function exportWardleyOwmWithWarnings(board, options = {}) {
597
639
  if (methodNodes.length > 0) {
598
640
  warnings.push(`${methodNodes.length} component${methodNodes.length === 1 ? ' carries' : 's carry'} a method (build / buy / outsource). OWM writes a method as a decorator this export cannot tell apart, so ${methodNodes.length === 1 ? 'it was' : 'they were'} written as plain components.`);
599
641
  }
642
+ if (porterNodes.length > 0) {
643
+ warnings.push(`${porterNodes.length} Porter's forces glyph${porterNodes.length === 1 ? '' : 's'} could not be written: OWM has no word for an external competition force, so ${porterNodes.length === 1 ? 'it was' : 'they were'} left out.`);
644
+ }
645
+ if (areaNodes.length > 0) {
646
+ warnings.push(`${areaNodes.length} area${areaNodes.length === 1 ? '' : 's'} could not be written: OWM has no word for a zone of the map, so ${areaNodes.length === 1 ? 'it was' : 'they were'} left out.`);
647
+ }
600
648
  if (looseLinks.length > 0) {
601
649
  warnings.push(`${looseLinks.length} link${looseLinks.length === 1 ? ' has an end' : 's have ends'} that is loose or attached to something that is not a Wardley artefact. A link names two components, so ${looseLinks.length === 1 ? 'it was' : 'they were'} left out.`);
602
650
  }
package/dist/import.d.ts CHANGED
@@ -44,10 +44,11 @@ import { OWM_SCOPE, WARDLEY_OWM_FORMAT_ID } from './export.js';
44
44
  * ## Three states, and the middle one is where the file survives
45
45
  *
46
46
  * **Mapped** is what the pack draws: `component`, `anchor`, `market`,
47
- * `ecosystem`, `pipeline`, `note`, `evolve`, `title`, and the `->` links.
47
+ * `ecosystem`, `accelerator`, `deaccelerator`, `pipeline`, `note`, `evolve`,
48
+ * `title`, and the `->` links.
48
49
  * **Carried** is every other statement — `style`, `annotation`, `attitudes`
49
50
  * (`pioneers` / `settlers` / `townplanners`), `submap`, `url`, `size`,
50
- * `accelerator`, the axis-label overrides, the flow links (`+>`, `+<`, `+<>`,
51
+ * the axis-label overrides, the flow links (`+>`, `+<`, `+<>`,
51
52
  * `+'…'>`), a link carrying a `;` context, and every `//` comment — kept
52
53
  * verbatim in `interchange.owm.children['@document']` on the map's background
53
54
  * element (D6) and written back, in order, at the end of the next export.
@@ -67,6 +68,7 @@ import { OWM_SCOPE, WARDLEY_OWM_FORMAT_ID } from './export.js';
67
68
  * | what | state | after a round trip |
68
69
  * | ---------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
69
70
  * | `component` / `anchor` / `market` / `ecosystem` and their coordinates | mapped | drawn, and written back from the drawing, to two decimals |
71
+ * | `accelerator` / `deaccelerator` and their coordinates | mapped | drawn as the climate arrow, and written back under OWM's own spelling (`deaccelerator` for a decelerator) |
70
72
  * | the NAME, which in this format is the identity | mapped | given back verbatim, quoted exactly as it needs to be — the fixed point |
71
73
  * | `pipeline Name [m1, m2]` | mapped | drawn as the body + handle composite; `[m1, m2]` written back from the body's two edges |
72
74
  * | `note Text [v, e]` | mapped | a free text on the map, written back from its centre |
@@ -74,7 +76,7 @@ import { OWM_SCOPE, WARDLEY_OWM_FORMAT_ID } from './export.js';
74
76
  * | `A->B` links | mapped | a `wardley:dependency` connector, consumer → what it needs, never inverted |
75
77
  * | `title` | mapped | consumed as the board's own name; written back from the board's name (see the two rows below) |
76
78
  * | a mapped line's TAIL — `label [x, y]`, `(build)`, `inertia`, comments | carried | invisible on the canvas, re-appended to the very line it came off |
77
- * | `style`, `annotation(s)`, `attitudes`, `submap`, `url`, `size`, `accelerator`, the axis-label overrides | carried | invisible on the canvas, re-emitted verbatim at the end of the file |
79
+ * | `style`, `annotation(s)`, `attitudes`, `submap`, `url`, `size`, the axis-label overrides | carried | invisible on the canvas, re-emitted verbatim at the end of the file |
78
80
  * | flow links (`+>`, `+<`, `+<>`, `+'…'>`) and a link with a `;` context | carried | not drawn — a flow is not a dependency — and re-emitted verbatim |
79
81
  * | a `pipeline` with a `{ … }` body | carried | the block is kept whole and written back whole; the pack draws no pipeline children |
80
82
  * | `//` comments | carried | re-emitted verbatim, at the end |
package/dist/import.js CHANGED
@@ -1,7 +1,8 @@
1
- import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
1
+ import { ConnectorMode, FontFamily, FontWeight, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
2
2
  import { WARDLEY_BACKGROUND } from './background.js';
3
3
  import { OWM_DEFAULT_MAP_HEIGHT, OWM_DEFAULT_MAP_WIDTH, OWM_LABEL_HEIGHT, OWM_LABEL_WIDTH, OWM_SCOPE, OWM_TAIL_ATTR, OWM_KEYWORDS, OWM_TITLE_ATTR, owmDefaultPlot, owmPointOf, WARDLEY_OWM_FORMAT_ID, } from './export.js';
4
4
  import { HANDLE_SIZE, LABEL_GAP, LABEL_FONT_SIZE, LINK_GREY, LINK_STROKE_WIDTH, MARKET_DOT_RING, MARKET_DOT_SIZE, MARKET_DOT_STROKE_WIDTH, MARKET_LINK_COLOR, MARKET_LINK_WIDTH, MARKET_SIZE, ECOSYSTEM_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, PIPELINE_FILL, PIPELINE_HEIGHT, WARDLEY_RED, } from './node/consts.js';
5
+ import { WARDLEY_NODE_SIZE, wardleyNodeProps } from './presets.js';
5
6
  import { WARDLEY_ROLE } from './roles.js';
6
7
  /**
7
8
  * An OnlineWardleyMaps (OWM) DSL document, read as a Wardley map — the inverse
@@ -47,10 +48,11 @@ import { WARDLEY_ROLE } from './roles.js';
47
48
  * ## Three states, and the middle one is where the file survives
48
49
  *
49
50
  * **Mapped** is what the pack draws: `component`, `anchor`, `market`,
50
- * `ecosystem`, `pipeline`, `note`, `evolve`, `title`, and the `->` links.
51
+ * `ecosystem`, `accelerator`, `deaccelerator`, `pipeline`, `note`, `evolve`,
52
+ * `title`, and the `->` links.
51
53
  * **Carried** is every other statement — `style`, `annotation`, `attitudes`
52
54
  * (`pioneers` / `settlers` / `townplanners`), `submap`, `url`, `size`,
53
- * `accelerator`, the axis-label overrides, the flow links (`+>`, `+<`, `+<>`,
55
+ * the axis-label overrides, the flow links (`+>`, `+<`, `+<>`,
54
56
  * `+'…'>`), a link carrying a `;` context, and every `//` comment — kept
55
57
  * verbatim in `interchange.owm.children['@document']` on the map's background
56
58
  * element (D6) and written back, in order, at the end of the next export.
@@ -70,6 +72,7 @@ import { WARDLEY_ROLE } from './roles.js';
70
72
  * | what | state | after a round trip |
71
73
  * | ---------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
72
74
  * | `component` / `anchor` / `market` / `ecosystem` and their coordinates | mapped | drawn, and written back from the drawing, to two decimals |
75
+ * | `accelerator` / `deaccelerator` and their coordinates | mapped | drawn as the climate arrow, and written back under OWM's own spelling (`deaccelerator` for a decelerator) |
73
76
  * | the NAME, which in this format is the identity | mapped | given back verbatim, quoted exactly as it needs to be — the fixed point |
74
77
  * | `pipeline Name [m1, m2]` | mapped | drawn as the body + handle composite; `[m1, m2]` written back from the body's two edges |
75
78
  * | `note Text [v, e]` | mapped | a free text on the map, written back from its centre |
@@ -77,7 +80,7 @@ import { WARDLEY_ROLE } from './roles.js';
77
80
  * | `A->B` links | mapped | a `wardley:dependency` connector, consumer → what it needs, never inverted |
78
81
  * | `title` | mapped | consumed as the board's own name; written back from the board's name (see the two rows below) |
79
82
  * | a mapped line's TAIL — `label [x, y]`, `(build)`, `inertia`, comments | carried | invisible on the canvas, re-appended to the very line it came off |
80
- * | `style`, `annotation(s)`, `attitudes`, `submap`, `url`, `size`, `accelerator`, the axis-label overrides | carried | invisible on the canvas, re-emitted verbatim at the end of the file |
83
+ * | `style`, `annotation(s)`, `attitudes`, `submap`, `url`, `size`, the axis-label overrides | carried | invisible on the canvas, re-emitted verbatim at the end of the file |
81
84
  * | flow links (`+>`, `+<`, `+<>`, `+'…'>`) and a link with a `;` context | carried | not drawn — a flow is not a dependency — and re-emitted verbatim |
82
85
  * | a `pipeline` with a `{ … }` body | carried | the block is kept whole and written back whole; the pack draws no pipeline children |
83
86
  * | `//` comments | carried | re-emitted verbatim, at the end |
@@ -217,12 +220,21 @@ const MATURITY = String.raw `[0-9]*\.[0-9]+`;
217
220
  const BEFORE_MATURITY = new RegExp(String.raw `^(.*?)(\s+${MATURITY})(\s.*)?$`);
218
221
  const LEADING_MATURITY = new RegExp(String.raw `^\s*(${MATURITY})`);
219
222
  /* ── The statements a document is made of ─────────────────────────────── */
220
- /** The keywords that declare a positioned artefact, and the kind each is. */
223
+ /**
224
+ * The keywords that declare a positioned artefact, and the kind each is.
225
+ *
226
+ * `deaccelerator` is OWM's own spelling of a decelerator — the keyword the
227
+ * reference parser claims — so the KEY is the format's word and the VALUE is
228
+ * this canvas's kind. It is the one row where the two differ, and the reason
229
+ * this is a table rather than an identity.
230
+ */
221
231
  const NODE_KEYWORDS = {
222
232
  component: 'component',
223
233
  anchor: 'anchor',
224
234
  market: 'market',
225
235
  ecosystem: 'ecosystem',
236
+ accelerator: 'accelerator',
237
+ deaccelerator: 'decelerator',
226
238
  };
227
239
  /* ── The reader ───────────────────────────────────────────────────────── */
228
240
  /**
@@ -724,7 +736,12 @@ function payload(parts) {
724
736
  return { [WARDLEY_OWM_FORMAT_ID]: carried };
725
737
  }
726
738
  /** A name, as the free text element this canvas writes one as. */
727
- function label(text, x, y, textAlign, color = NODE_STROKE) {
739
+ function label(text, x, y, textAlign, color = NODE_STROKE,
740
+ // Regular by default, because that is what a value-chain name is. The one
741
+ // caller that passes anything else is the climate arrow below, and it does so
742
+ // for the reason `createWardleyAccelerator` does: an imported map and a drawn
743
+ // one must be the same document, down to the weight of the words.
744
+ fontWeight = FontWeight.Regular) {
728
745
  return {
729
746
  type: 'text',
730
747
  text,
@@ -734,12 +751,40 @@ function label(text, x, y, textAlign, color = NODE_STROKE) {
734
751
  color,
735
752
  fontFamily: FontFamily.Inter,
736
753
  fontSize: LABEL_FONT_SIZE,
754
+ fontWeight,
737
755
  textAlign,
738
756
  xywh: `[${x},${y},${OWM_LABEL_WIDTH},${OWM_LABEL_HEIGHT}]`,
739
757
  };
740
758
  }
741
759
  /** The circle (or composite) one positioned statement draws, plus its name. */
742
760
  function artefact(keyword, name, cx, cy, tail, mintHandle) {
761
+ // The two climate annotations are the only statements this reader draws as
762
+ // something other than a circle: a fat polygon, at the canonical size the
763
+ // palette gives one, with its name on the side its shaft is on. The presets
764
+ // own both facts, so a map that arrives from a file and one drawn by hand are
765
+ // the same element.
766
+ const kind = NODE_KEYWORDS[keyword];
767
+ if (kind === 'accelerator' || kind === 'decelerator') {
768
+ const { w, h } = WARDLEY_NODE_SIZE[kind];
769
+ const arrow = {
770
+ ...wardleyNodeProps(kind, {
771
+ xywh: `[${cx - w / 2},${cy - h / 2},${w},${h}]`,
772
+ }),
773
+ interchange: payload({ id: name, tail }),
774
+ };
775
+ const rightwards = kind === 'accelerator';
776
+ return [
777
+ arrow,
778
+ label(name, rightwards
779
+ ? cx + w / 2 + LABEL_GAP
780
+ : cx - w / 2 - LABEL_GAP - OWM_LABEL_WIDTH, cy - OWM_LABEL_HEIGHT / 2, rightwards ? TextAlign.Left : TextAlign.Right, NODE_STROKE,
781
+ // SemiBold, exactly as the sub-menu draws one. The recette of #212
782
+ // caught the two paths disagreeing: a climate arrow is an annotation
783
+ // laid over a map already full of names, and where it came from is not
784
+ // something the reader should be able to see.
785
+ FontWeight.SemiBold),
786
+ ];
787
+ }
743
788
  const diameter = keyword === 'anchor'
744
789
  ? OWM_ANCHOR_SIZE
745
790
  : keyword === 'market'
@@ -749,8 +794,8 @@ function artefact(keyword, name, cx, cy, tail, mintHandle) {
749
794
  : NODE_SIZE;
750
795
  const circle = {
751
796
  type: 'wardleyNode',
752
- kind: NODE_KEYWORDS[keyword],
753
- role: WARDLEY_ROLE[NODE_KEYWORDS[keyword]],
797
+ kind,
798
+ role: WARDLEY_ROLE[kind],
754
799
  shapeType: 'ellipse',
755
800
  filled: true,
756
801
  fillColor: NODE_FILL,
package/dist/index.d.ts CHANGED
@@ -2,11 +2,14 @@ export { WARDLEY_ROLE, WARDLEY_ROLES, type WardleyRole, type WardleyRoleId, } fr
2
2
  export { WARDLEY_AUDIT_CRITERIA } from './audit-criteria.js';
3
3
  export { wardleyCommandIcons, wardleyCommands } from './commands.js';
4
4
  export { wardleyTranslationEntries } from './translations.js';
5
- export { WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID, WARDLEY_TAG_DEFS, } from './natures.js';
5
+ export { WARDLEY_COMPETITION, WARDLEY_COMPETITION_TAG_ID, WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID, WARDLEY_TAG_DEFS, } from './natures.js';
6
+ export { competitionOfPorterLetter, porterLetterOfCompetition, type WardleyPorterLetter, } from './porter-competition.js';
6
7
  export { WARDLEY_PROFILES } from './profiles.js';
7
8
  export { WARDLEY_NUDGES } from './nudges.js';
8
9
  export { WARDLEY_NAMING_CONVENTIONS, WARDLEY_READING } from './reading.js';
9
10
  export { WARDLEY_RULES } from './rules.js';
11
+ export { WARDLEY_AREA_SIZE, wardleyAreaBox, wardleyAreaProps, type WardleyAreaShape, type WardleyArtefactKind, wardleyCanonicalBox, wardleyCenteredBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, type WardleyLabelledKind, WARDLEY_MORPHABLE_KINDS, wardleyMorphClears, wardleyMorphProps, wardleyNodeProps, type WardleyPorterArrow, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
12
+ export { WARDLEY_MORPH_FAMILIES, WARDLEY_MORPH_SPEC, type WardleyMorphKind, wardleyMorphComposite, wardleyMorphedLabel, wardleyNodeOfComponent, } from './morph.js';
10
13
  /**
11
14
  * The map's own frame, declared. Public because a host that lays elements out
12
15
  * against the plot — labre-mcp does, and so does anything that generates a map
package/dist/index.js CHANGED
@@ -2,11 +2,26 @@ export { WARDLEY_ROLE, WARDLEY_ROLES, } from './roles.js';
2
2
  export { WARDLEY_AUDIT_CRITERIA } from './audit-criteria.js';
3
3
  export { wardleyCommandIcons, wardleyCommands } from './commands.js';
4
4
  export { wardleyTranslationEntries } from './translations.js';
5
- export { WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID, WARDLEY_TAG_DEFS, } from './natures.js';
5
+ export { WARDLEY_COMPETITION, WARDLEY_COMPETITION_TAG_ID, WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID, WARDLEY_TAG_DEFS, } from './natures.js';
6
+ // The letter and the tag, related in both directions. Exported for the reason
7
+ // the presets are: a host laying out or reading a map without an editor —
8
+ // labre-mcp does — must be able to say which force a glyph marks without
9
+ // re-deriving the mapping from the three labels.
10
+ export { competitionOfPorterLetter, porterLetterOfCompetition, } from './porter-competition.js';
6
11
  export { WARDLEY_PROFILES } from './profiles.js';
7
12
  export { WARDLEY_NUDGES } from './nudges.js';
8
13
  export { WARDLEY_NAMING_CONVENTIONS, WARDLEY_READING } from './reading.js';
9
14
  export { WARDLEY_RULES } from './rules.js';
15
+ // What a Wardley artefact is BORN as, shared by the palette and — since the
16
+ // morph — by the toolbar that says one artefact differently, so the two can
17
+ // never disagree about what a market looks like. The sizes are the notation
18
+ // itself, which is why a host laying out a map without an editor needs them.
19
+ export { WARDLEY_AREA_SIZE, wardleyAreaBox, wardleyAreaProps, wardleyCanonicalBox, wardleyCenteredBox, wardleyHandleBox, wardleyHandleProps, wardleyMarketDotBoxes, wardleyMarketDotProps, wardleyMarketLinkPairs, wardleyMarketLinkProps, WARDLEY_NODE_LABEL, WARDLEY_NODE_SIZE, WARDLEY_MORPHABLE_KINDS, wardleyMorphClears, wardleyMorphProps, wardleyNodeProps, wardleyPorterArrowProps, wardleyPorterArrows, wardleyPorterLetterProps, } from './presets.js';
20
+ // What an artefact may BECOME: the declared family, the spec the generic morph
21
+ // module is registered with, the resolution from a selected group to the node
22
+ // inside it, and the two rules a composite morph owes its own parts. Data a
23
+ // host can read without an editor.
24
+ export { WARDLEY_MORPH_FAMILIES, WARDLEY_MORPH_SPEC, wardleyMorphComposite, wardleyMorphedLabel, wardleyNodeOfComponent, } from './morph.js';
10
25
  /**
11
26
  * The map's own frame, declared. Public because a host that lays elements out
12
27
  * against the plot — labre-mcp does, and so does anything that generates a map
package/dist/legend.d.ts CHANGED
@@ -1,11 +1,37 @@
1
1
  import { type WardleyBackgroundElementModel } from '@formicoidea/labre-core/model';
2
2
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
3
+ /** A box in the panel, relative to the panel's top-left, plus its words. */
4
+ interface PorterPanelBox {
5
+ xywh: [number, number, number, number];
6
+ label: string;
7
+ }
8
+ interface PorterPanelLayout {
9
+ w: number;
10
+ h: number;
11
+ title: [number, number, number, number];
12
+ /** The glyph's centre. */
13
+ center: [number, number];
14
+ radius: number;
15
+ boxes: PorterPanelBox[];
16
+ caption: [number, number, number, number];
17
+ }
18
+ /**
19
+ * The panel's geometry for a given width — the PO's reference drawing, as
20
+ * numbers.
21
+ *
22
+ * A pure function so the layout can be asserted without a surface, and so the
23
+ * one thing that must hold — that none of the four boxes overlaps the glyph
24
+ * they surround — is checkable rather than eyeballed.
25
+ */
26
+ export declare function porterPanelLayout(w: number): PorterPanelLayout;
3
27
  /**
4
28
  * Build a "Legend" group from real, editable elements (white rect frame +
5
29
  * "Legend" text + one row of [real component glyph + description text] per
6
30
  * Wardley component TYPE present inside the background's perimeter + a
7
- * gradient-meaning block when the background is a gradient variant). A snapshot
31
+ * gradient-meaning block when the background is a gradient variant + the
32
+ * five-forces panel when a Porter's-forces glyph is on the map). A snapshot
8
33
  * is created on each call; everything is grouped so it can be moved / resized /
9
34
  * edited and is dropped bottom-left of the background.
10
35
  */
11
36
  export declare function createWardleyLegend(std: BlockStdScope, bg: WardleyBackgroundElementModel): void;
37
+ export {};