@formicoidea/labre-framework-wardley 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/actions.d.ts +63 -20
  2. package/dist/actions.js +190 -57
  3. package/dist/audit-criteria.d.ts +31 -0
  4. package/dist/audit-criteria.js +90 -0
  5. package/dist/background.d.ts +14 -0
  6. package/dist/background.js +338 -0
  7. package/dist/commands.d.ts +9 -0
  8. package/dist/commands.js +352 -0
  9. package/dist/consts.d.ts +10 -4
  10. package/dist/consts.js +10 -9
  11. package/dist/descriptor.d.ts +8 -3
  12. package/dist/descriptor.js +6 -3
  13. package/dist/element-renderer.d.ts +8 -5
  14. package/dist/element-renderer.js +12 -147
  15. package/dist/element-view.d.ts +8 -4
  16. package/dist/element-view.js +30 -22
  17. package/dist/export.d.ts +211 -0
  18. package/dist/export.js +655 -0
  19. package/dist/gradient.d.ts +6 -11
  20. package/dist/gradient.js +59 -48
  21. package/dist/import.d.ts +116 -0
  22. package/dist/import.js +905 -0
  23. package/dist/index.d.ts +27 -1
  24. package/dist/index.js +31 -1
  25. package/dist/interchange.d.ts +80 -0
  26. package/dist/interchange.js +138 -0
  27. package/dist/legend.js +8 -0
  28. package/dist/natures.d.ts +50 -0
  29. package/dist/natures.js +93 -0
  30. package/dist/node/node-renderer.js +1 -1
  31. package/dist/nudges.d.ts +41 -0
  32. package/dist/nudges.js +69 -0
  33. package/dist/profiles.d.ts +2 -0
  34. package/dist/profiles.js +87 -0
  35. package/dist/reading.d.ts +3 -0
  36. package/dist/reading.js +129 -0
  37. package/dist/roles.d.ts +50 -0
  38. package/dist/roles.js +132 -0
  39. package/dist/rules.d.ts +2 -0
  40. package/dist/rules.js +286 -0
  41. package/dist/templates/index.js +66 -10
  42. package/dist/templates/maps.js +146 -22
  43. package/dist/toolbar/config.js +3 -1
  44. package/dist/toolbar/icons.d.ts +20 -0
  45. package/dist/toolbar/icons.js +34 -0
  46. package/dist/toolbar/senior-tool.js +1 -0
  47. package/dist/toolbar/wardley-menu.d.ts +8 -15
  48. package/dist/toolbar/wardley-menu.js +8 -136
  49. package/dist/toolbar/wardley-senior-button.js +12 -6
  50. package/dist/translations.d.ts +16 -0
  51. package/dist/translations.js +24 -0
  52. package/dist/view.d.ts +17 -0
  53. package/dist/view.js +127 -13
  54. package/package.json +2 -2
  55. package/dist/label-layout.d.ts +0 -20
  56. package/dist/label-layout.js +0 -72
  57. package/dist/shortcuts.d.ts +0 -2
  58. package/dist/shortcuts.js +0 -37
@@ -0,0 +1,3 @@
1
+ import type { ReadingNamingConvention, ReadingProfile } from '@formicoidea/labre-core/blocks/surface';
2
+ export declare const WARDLEY_NAMING_CONVENTIONS: readonly ReadingNamingConvention[];
3
+ export declare const WARDLEY_READING: ReadingProfile;
@@ -0,0 +1,129 @@
1
+ import { WARDLEY_BACKGROUND } from './background.js';
2
+ import { WARDLEY_NATURE, WARDLEY_NATURE_TAG_ID } from './natures.js';
3
+ import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
4
+ /**
5
+ * What Wardley lets the tool read of a component (MF3, the reversed reading).
6
+ *
7
+ * DATA, like the rules and the profiles beside it: the engine
8
+ * (`@labre/affine-block-surface`) knows how to read a role, a tag, a typed edge
9
+ * and a declared background — it knows nothing about Wardley. Everything below
10
+ * is already stated somewhere else in this framework and is merely POINTED AT
11
+ * here: the role vocabulary (`roles.ts`), the nature tag (`natures.ts`), the
12
+ * dependency edge (ADR 0010 § 2 tier 2) and the map's own zones
13
+ * (`background.ts`).
14
+ *
15
+ * Registered from the FLAG-GATED view extension: reading a map is tooling, so a
16
+ * board whose Wardley flag is off keeps every element it has and simply stops
17
+ * being read (ADR 0009).
18
+ */
19
+ /**
20
+ * ## The naming convention, and why it is one motif rather than a grammar
21
+ *
22
+ * Wardley's four natures answer "what KIND of thing is this?", and two of the
23
+ * four answers are visible in the NAME long before anyone opens a picker: an
24
+ * activity is something that is DONE, and the other three are things that
25
+ * ARE — something recorded, a way of doing, something known.
26
+ *
27
+ * So the convention retained is exactly one motif — **does the name read as an
28
+ * action?** — expressed as the English gerund (`…ing`), applied POSITIVELY to
29
+ * `activity` and NEGATIVELY to the three others:
30
+ *
31
+ * | nature | expected | example |
32
+ * | --------- | --------------------------------- | -------------------- |
33
+ * | activity | reads as an action | "Brewing tea" |
34
+ * | data | does NOT read as an action | "Customer register" |
35
+ * | practice | does NOT read as an action | "Agile method" |
36
+ * | knowledge | does NOT read as an action | "Thermodynamics" |
37
+ *
38
+ * One motif, four entries, no word list. A vocabulary of "practice-ish nouns"
39
+ * would be a dictionary the library would then own, in one language, and would
40
+ * be wrong for the first client who names things in their own — whereas the
41
+ * gerund is a single, checkable shape a user can predict from the sentence they
42
+ * are shown.
43
+ *
44
+ * ## The motif is ENGLISH, and it says so
45
+ *
46
+ * `\p{L}+ing` is a fact about English, so every entry below declares
47
+ * `lang: 'en'` and the engine keeps quiet unless the host says it is serving
48
+ * English. That is not a detail: on a board named in French the same motif is
49
+ * wrong in BOTH directions — "Facturation" on an activity would be told to use
50
+ * a verb, and "Planning", "Sourcing" or "Reporting" on data would be told they
51
+ * read as actions. An out-of-scope suggestion is worse than none, because it is
52
+ * confident.
53
+ *
54
+ * Extending the coverage is adding entries, not changing code: one more
55
+ * convention per `valueId` with its own `lang` and its own motif (French, say,
56
+ * would key on the `-tion`/`-ment` nominalisations against an infinitive), and
57
+ * the first one in scope wins. A host that declares no language gets silence,
58
+ * which is the honest answer to "I do not know what language these words are
59
+ * in".
60
+ *
61
+ * It is a SUGGESTION and never a verdict: it produces no violation, blocks no
62
+ * gesture, and the panel prints the wording below rather than a red mark. A
63
+ * component named "Tea" whose nature is `activity` is very often correct — the
64
+ * architect writing shorthand — and the reading's job is to ask, not to insist.
65
+ *
66
+ * The wording travels as an i18n key with an English fallback, like every other
67
+ * declaration in this framework: the host localises it, and a host with no
68
+ * catalogue still reads a real sentence.
69
+ */
70
+ const ACTION_MOTIF = String.raw `\b\p{L}+ing\b`;
71
+ const naming = (valueId, expectsAction, key, fallback) => ({
72
+ valueId,
73
+ // The gerund is a fact about English: out of that scope the engine says
74
+ // nothing rather than something confident and wrong.
75
+ lang: 'en',
76
+ // The negative form is the same motif under a lookahead, so the two can never
77
+ // drift apart: change what "reads as an action" means and both sides move.
78
+ // `[\s\S]` rather than `.` because a canvas label wraps, and a name on two
79
+ // lines must be judged on what it says, not on where it broke.
80
+ pattern: expectsAction
81
+ ? ACTION_MOTIF
82
+ : `^(?![\\s\\S]*${ACTION_MOTIF})[\\s\\S]+$`,
83
+ hintKey: key,
84
+ hintFallback: fallback,
85
+ });
86
+ export const WARDLEY_NAMING_CONVENTIONS = [
87
+ naming(WARDLEY_NATURE.activity, true, 'com.labre.wardley.reading.naming.activity', 'An activity is something that is done — name it with a verb ("Brewing tea"), not with a thing.'),
88
+ naming(WARDLEY_NATURE.data, false, 'com.labre.wardley.reading.naming.data', 'Data is something recorded — name it as a thing ("Customer register"), not as an action.'),
89
+ naming(WARDLEY_NATURE.practice, false, 'com.labre.wardley.reading.naming.practice', 'A practice is a way of doing — name the practice ("Agile method"), not the doing.'),
90
+ naming(WARDLEY_NATURE.knowledge, false, 'com.labre.wardley.reading.naming.knowledge', 'Knowledge is something known — name what is known ("Thermodynamics"), not an activity.'),
91
+ ];
92
+ export const WARDLEY_READING = {
93
+ id: 'wardley',
94
+ framework: 'wardley',
95
+ roles: WARDLEY_ROLES,
96
+ // The subject is a component. `market` and `ecosystem` specialise it and are
97
+ // read for free through `roleIsA` — that is the entire reason role hierarchy
98
+ // is data. The `anchor` (a user / need) is deliberately NOT a child of
99
+ // `component` and is deliberately not read: a need has no nature and no
100
+ // phase, it has a demand.
101
+ appliesTo: WARDLEY_ROLE.component,
102
+ // On this canvas a name IS a separate free text element grouped with the
103
+ // node — the same fact W3 measures overlaps against.
104
+ labelRole: WARDLEY_ROLE.label,
105
+ nature: {
106
+ tagId: WARDLEY_NATURE_TAG_ID,
107
+ conventions: WARDLEY_NAMING_CONVENTIONS,
108
+ },
109
+ // The value-chain link. ADR 0010 § 2: `source` is the subject of the role's
110
+ // verb, and this role's verb is "depends on" — so the source is the consumer
111
+ // and the target is what it needs.
112
+ relation: { edgeRole: WARDLEY_ROLE.dependency },
113
+ frame: {
114
+ backgroundRole: WARDLEY_ROLE.map,
115
+ background: WARDLEY_BACKGROUND,
116
+ // Evolution runs along the plot's x axis; its four zones ARE the phases.
117
+ axis: 'x',
118
+ },
119
+ /**
120
+ * The two record properties a reading may compare itself against.
121
+ *
122
+ * Host keys, and plain ones on purpose: the library does not name a property
123
+ * of somebody else's document, it states which two it would read IF the host
124
+ * ships them (and lists them in `hoverFields`). A host whose record spells
125
+ * them differently gets no comparison and no drift — silence, which is the
126
+ * correct answer to "I cannot see the record's nature".
127
+ */
128
+ recordKeys: { nature: 'nature', phase: 'phase' },
129
+ };
@@ -0,0 +1,50 @@
1
+ import type { WardleyNodeKind } from '@formicoidea/labre-core/model';
2
+ import type { RoleDefs } from '@formicoidea/labre-core/std/gfx';
3
+ /**
4
+ * Wardley role vocabulary (PF13.1).
5
+ *
6
+ * Roles are the semantic identity of a Wardley artefact — no rule will ever
7
+ * look at a shape type. They mirror {@link WardleyNodeKind}, which the user
8
+ * already picks when choosing the artefact in the toolbox, plus the typed
9
+ * `dependency` EDGE carried by the Wardley link connector.
10
+ *
11
+ * Hierarchy is DATA (`parent`), never TS inheritance: `market` and `ecosystem`
12
+ * specialise `component`, so a rule written on `wardley:component` applies to
13
+ * them for free (see `roleIsA`). The `anchor` (user / need) is a role of its
14
+ * own — a component with no need of its own — and is deliberately NOT a child
15
+ * of `component`.
16
+ */
17
+ /**
18
+ * ## Revision of decision #71 (PF13.4, 01/08/2026)
19
+ *
20
+ * PF1 left three artefacts NEUTRAL — the evolution arrow ("a movement
21
+ * annotation, not a dependency"), the inertia bar and the node labels — on the
22
+ * ground that nothing was written on them yet. That was right while the only
23
+ * rule was the pilot; it is wrong now that the framework has rules about them:
24
+ * an element with no role is never evaluated (proportionality, PRD principle
25
+ * 8), so leaving these neutral would have meant W1, W2 and W3 could not exist.
26
+ *
27
+ * The reversal writes new VALUES into the existing `role` field. No schema
28
+ * change, no migration, and no backfill of documents already drawn: a map
29
+ * authored before today carries arrows and bars with no role, so it raises
30
+ * nothing — which is the same promise every earlier role made.
31
+ */
32
+ /** Every role this framework declares. */
33
+ export type WardleyRole = WardleyNodeKind | 'dependency' | 'map' | 'change-arrow' | 'inertia' | 'label';
34
+ export type WardleyRoleId = `wardley:${WardleyRole}`;
35
+ /** Role ids, keyed by the `kind` used at the creation sites. */
36
+ export declare const WARDLEY_ROLE: {
37
+ readonly component: "wardley:component";
38
+ readonly anchor: "wardley:anchor";
39
+ readonly pipeline: "wardley:pipeline";
40
+ readonly handle: "wardley:handle";
41
+ readonly market: "wardley:market";
42
+ readonly ecosystem: "wardley:ecosystem";
43
+ readonly method: "wardley:method";
44
+ readonly dependency: "wardley:dependency";
45
+ readonly map: "wardley:map";
46
+ readonly changeArrow: "wardley:change-arrow";
47
+ readonly inertia: "wardley:inertia";
48
+ readonly label: "wardley:label";
49
+ };
50
+ export declare const WARDLEY_ROLES: RoleDefs;
package/dist/roles.js ADDED
@@ -0,0 +1,132 @@
1
+ /** Role ids, keyed by the `kind` used at the creation sites. */
2
+ export const WARDLEY_ROLE = {
3
+ component: 'wardley:component',
4
+ anchor: 'wardley:anchor',
5
+ pipeline: 'wardley:pipeline',
6
+ handle: 'wardley:handle',
7
+ market: 'wardley:market',
8
+ ecosystem: 'wardley:ecosystem',
9
+ method: 'wardley:method',
10
+ dependency: 'wardley:dependency',
11
+ map: 'wardley:map',
12
+ changeArrow: 'wardley:change-arrow',
13
+ inertia: 'wardley:inertia',
14
+ label: 'wardley:label',
15
+ };
16
+ const DEFS = [
17
+ {
18
+ id: WARDLEY_ROLE.component,
19
+ kind: 'node',
20
+ labelKey: 'com.labre.wardley.role.component',
21
+ },
22
+ // A user / need: a component with no need of its own, hence its own role
23
+ // rather than a specialisation of `component`.
24
+ {
25
+ id: WARDLEY_ROLE.anchor,
26
+ kind: 'node',
27
+ labelKey: 'com.labre.wardley.role.anchor',
28
+ },
29
+ // The pipeline body. Connections go through its handle, never the body.
30
+ {
31
+ id: WARDLEY_ROLE.pipeline,
32
+ kind: 'node',
33
+ labelKey: 'com.labre.wardley.role.pipeline',
34
+ },
35
+ {
36
+ id: WARDLEY_ROLE.handle,
37
+ kind: 'node',
38
+ labelKey: 'com.labre.wardley.role.handle',
39
+ },
40
+ {
41
+ id: WARDLEY_ROLE.market,
42
+ parent: WARDLEY_ROLE.component,
43
+ kind: 'node',
44
+ labelKey: 'com.labre.wardley.role.market',
45
+ },
46
+ {
47
+ id: WARDLEY_ROLE.ecosystem,
48
+ parent: WARDLEY_ROLE.component,
49
+ kind: 'node',
50
+ labelKey: 'com.labre.wardley.role.ecosystem',
51
+ },
52
+ {
53
+ id: WARDLEY_ROLE.method,
54
+ kind: 'node',
55
+ labelKey: 'com.labre.wardley.role.method',
56
+ },
57
+ /**
58
+ * The value-chain link: "A depends on B".
59
+ *
60
+ * The comment above was the whole of the convention until `docs/adr/0010`
61
+ * made it DATA. Tier 1 of that ADR is generic — `source` is the subject of
62
+ * the role's verb, `target` its object — and tier 2 is this `direction`
63
+ * block: the verb of THIS role is "depends on", so its source is the
64
+ * consumer and its target is what the consumer needs. Needs descend from
65
+ * source to target; value flows back up. W4 evaluates exactly that reading,
66
+ * and the tool hint below is what makes it a statement the user made rather
67
+ * than a by-product of which end their finger landed on first.
68
+ */
69
+ {
70
+ id: WARDLEY_ROLE.dependency,
71
+ kind: 'edge',
72
+ labelKey: 'com.labre.wardley.role.dependency',
73
+ labelFallback: 'Dependency',
74
+ direction: {
75
+ verbKey: 'com.labre.wardley.role.dependency.verb',
76
+ verbFallback: 'depends on',
77
+ gestureHintKey: 'com.labre.wardley.role.dependency.gesture',
78
+ gestureHintFallback: 'Drag from the component that has the need to what it needs.',
79
+ },
80
+ },
81
+ // The map itself: the axes frame the other roles are positioned against.
82
+ // A frame, not a component, so it specialises nothing — a rule written on
83
+ // `wardley:component` must never match the map it measures against.
84
+ {
85
+ id: WARDLEY_ROLE.map,
86
+ kind: 'node',
87
+ labelKey: 'com.labre.wardley.role.map',
88
+ },
89
+ // The change (evolution) arrow: "this is where it is going". An EDGE, and
90
+ // deliberately not a specialisation of `dependency` — it says nothing about
91
+ // what depends on what, and W1 must not fall on a value-chain link.
92
+ {
93
+ id: WARDLEY_ROLE.changeArrow,
94
+ kind: 'edge',
95
+ labelKey: 'com.labre.wardley.role.change-arrow',
96
+ labelFallback: 'Evolution arrow',
97
+ // Tier 1 applies to every edge role, so this one states its own verb — and
98
+ // it is NOT "depends on". An arrow says where a component is going: source
99
+ // is where it stands today, target where it is heading. W4 reads the
100
+ // dependency's verb and never this one.
101
+ direction: {
102
+ verbKey: 'com.labre.wardley.role.change-arrow.verb',
103
+ verbFallback: 'is evolving towards',
104
+ gestureHintKey: 'com.labre.wardley.role.change-arrow.gesture',
105
+ gestureHintFallback: 'Drag from where the component stands today to where it is heading.',
106
+ },
107
+ },
108
+ // The inertia bar: resistance to a movement, drawn ASTRIDE the phase divider
109
+ // the thing refuses to cross (W2). A plain filled rect on the canvas, which
110
+ // is exactly why it needs a role — nothing about its shape says what it
111
+ // means.
112
+ {
113
+ id: WARDLEY_ROLE.inertia,
114
+ kind: 'node',
115
+ labelKey: 'com.labre.wardley.role.inertia',
116
+ },
117
+ // The name written next to an artefact. A role of its own rather than a
118
+ // property of the node it labels: on this canvas a label IS a separate free
119
+ // text element, grouped with its node, and W3 is about where it lands.
120
+ //
121
+ // `text`, not `node`: a label is created 120 to 200 units wide whatever it
122
+ // reads, so its BOX is not a statement about anything the eye can see. The
123
+ // kind is what tells a rule to measure the words instead.
124
+ {
125
+ id: WARDLEY_ROLE.label,
126
+ kind: 'text',
127
+ labelKey: 'com.labre.wardley.role.label',
128
+ },
129
+ ];
130
+ // Null prototype: this is a lookup table keyed by ids that may one day come
131
+ // from host-supplied packs, so `defs['toString']` must not resolve.
132
+ export const WARDLEY_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 WARDLEY_RULES: readonly ValidationRule[];
package/dist/rules.js ADDED
@@ -0,0 +1,286 @@
1
+ import { WARDLEY_BACKGROUND } from './background.js';
2
+ import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
3
+ /**
4
+ * Wardley validation rules (PF13.4 / PF13.5 / PF13.6).
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. Adding a Wardley rule is adding an entry to this array.
9
+ *
10
+ * Registered from the flag-gated `WardleyViewExtension`, so switching the
11
+ * Wardley flag off removes the rules with the rest of the tooling — already
12
+ * drawn maps keep rendering, they simply stop being checked (see
13
+ * `docs/adr/0009`).
14
+ *
15
+ * ## The pilot rule is gone
16
+ *
17
+ * `wardley.component-outside-map` was the tracer bullet's rule: it existed to
18
+ * prove the engine, the affordance, the exceptions and the profiles could work
19
+ * end to end, never because a Wardley practitioner asked for it. Parking a node
20
+ * in the margin while you think is normal work, and PO decision of 01/08/2026
21
+ * removed it the moment there were real rules for the machinery to carry.
22
+ * Nothing was left behind: every test that exercised the pipework through it
23
+ * now exercises it through W1, W2 or W3.
24
+ *
25
+ * ## Severity, and a hole that is named rather than papered over
26
+ *
27
+ * W1 and W2 are the two the architect would put at `blocking-overridable`. They
28
+ * are declared `warning`, because NOTHING downstream implements a blocking
29
+ * level: no gesture is refused anywhere in this library, so shipping the value
30
+ * would be data claiming an effect that does not exist. The severity moves to
31
+ * `blocking-overridable` in the profiles, in one line each, the day the gesture
32
+ * refusal lands. Building it was explicitly out of this slice.
33
+ */
34
+ /**
35
+ * **W1** — a change arrow may not point against the sense of evolution.
36
+ *
37
+ * The whole grammar of a Wardley map is that things move left to right: a red
38
+ * arrow pointing back towards Genesis says the opposite of what the map means.
39
+ * Written on `wardley:change-arrow`, the role PF13.4 puts on the arrow at
40
+ * creation — value-chain links are a different role and are never touched.
41
+ *
42
+ * The 30° dead zone is deliberate. An arrow drawn straight up a value chain is
43
+ * neither with evolution nor against it, and one drawn slightly up-and-back is
44
+ * a hand that slipped, not a claim about the world. Only an arrow genuinely
45
+ * heading back — more than 120° off the axis' forward sense — is one.
46
+ */
47
+ const changeArrowAgainstEvolution = {
48
+ id: 'wardley.change-arrow-against-evolution',
49
+ framework: 'wardley',
50
+ family: 'orientation-against-axis',
51
+ severity: 'warning',
52
+ appliesTo: WARDLEY_ROLE.changeArrow,
53
+ roles: WARDLEY_ROLES,
54
+ messageKey: 'com.labre.wardley.validation.change-arrow-against-evolution',
55
+ messageFallback: 'This change arrow points against evolution.',
56
+ suggestionKey: 'com.labre.wardley.validation.change-arrow-against-evolution.suggestion',
57
+ suggestionFallback: 'Evolution runs left to right — turn the arrow towards the commodity end, or draw a dependency instead.',
58
+ version: 1,
59
+ provenance: {
60
+ source: 'recommendation',
61
+ reference: 'Wardley mapping — the evolution axis runs left to right, and a component moves with it',
62
+ },
63
+ backgroundRole: WARDLEY_ROLE.map,
64
+ // The frame's own declaration, carried as data exactly like `roles` is: it is
65
+ // where "which way does evolution run" is written, and the engine reads it
66
+ // rather than knowing anything about Wardley.
67
+ background: WARDLEY_BACKGROUND,
68
+ against: { axis: 'evolution', toleranceDeg: 30 },
69
+ };
70
+ /**
71
+ * **W2** — the inertia bar straddles a phase transition.
72
+ *
73
+ * > "The horizontal position of an inertia bar is only valid if it is ASTRIDE
74
+ * > two evolution phases, that is, superimposed on a dashed vertical axis."
75
+ * > — the PO, spelling the rule out on the recette of 02/08/2026
76
+ *
77
+ * Inertia is resistance to crossing a frontier. The bar is the frontier being
78
+ * refused, so it is drawn ON the divider; a bar parked in the middle of a phase
79
+ * marks nothing at all and becomes a black rectangle.
80
+ *
81
+ * ## What this rule used to ask, and no longer does
82
+ *
83
+ * Until this version it ALSO demanded a dependency under the bar, and reported
84
+ * that half first ("This inertia bar is not drawn on a dependency."). That was
85
+ * our reading of inertia, not the PO's rule, and it was wrong in both
86
+ * directions: a bar alone on a divider — a perfectly ordinary way to say "this
87
+ * whole column is stuck" — was flagged, while nothing in the sentence pointed at
88
+ * the position that actually decides the verdict. The carrier condition is gone
89
+ * entirely, with the second message that existed only to tell the two halves
90
+ * apart. One condition, one sentence.
91
+ *
92
+ * ## "Astride", as geometry
93
+ *
94
+ * The engine takes the bar's own horizontal EXTENT and asks whether it
95
+ * intersects the transition band — the divider widened by the map's declared
96
+ * `transitionBandWidth` (see {@link AttachmentDef.boundaryAxis}). That single
97
+ * overlap test says both halves of "superimposed on the axis": a bar wide enough
98
+ * to cover the divider genuinely has the line running through it, and a bar too
99
+ * thin to cover anything (the toolbox draws it eight units wide) is accepted
100
+ * inside the band the map itself declares around the frontier — Wardley's zone
101
+ * of punctuated equilibrium, where inertia lives.
102
+ *
103
+ * Measured on the extent rather than on the centre because "superimposed on the
104
+ * line" is a statement about ink; measured against a band declared as a RATIO of
105
+ * the plot because the same gesture must get the same verdict on a map somebody
106
+ * resized (the lesson of the 01/08/2026 recette).
107
+ */
108
+ const inertiaOffTransition = {
109
+ id: 'wardley.inertia-off-transition',
110
+ framework: 'wardley',
111
+ family: 'attachment',
112
+ severity: 'warning',
113
+ appliesTo: WARDLEY_ROLE.inertia,
114
+ roles: WARDLEY_ROLES,
115
+ // ONE sentence, because there is now one condition. The key is the rule's own
116
+ // id: the two keys it replaces named halves of a rule that no longer has any.
117
+ messageKey: 'com.labre.wardley.validation.inertia-off-transition',
118
+ messageFallback: 'This inertia bar sits inside a phase, not astride a phase transition.',
119
+ suggestionKey: 'com.labre.wardley.validation.inertia-off-transition.suggestion',
120
+ suggestionFallback: 'Inertia bites at a frontier — slide the bar sideways until it sits astride the dashed line between two evolution phases.',
121
+ // 3: the carrier condition is gone and the position is judged on the bar's
122
+ // extent — a different verdict on the same map, so a new version.
123
+ version: 3,
124
+ provenance: {
125
+ source: 'labre-convention',
126
+ reference: 'Labre convention (PO, recette of 02/08/2026) — Wardley names the zone, not the rule',
127
+ },
128
+ backgroundRole: WARDLEY_ROLE.map,
129
+ background: WARDLEY_BACKGROUND,
130
+ attachment: {
131
+ boundaryAxis: 'evolution',
132
+ },
133
+ };
134
+ /**
135
+ * **W3** — nodes and labels must not sit on top of each other.
136
+ *
137
+ * A readability rule, not a semantic one, which is why it is the softest of the
138
+ * three: a map you cannot read is useless, but a momentary overlap while you
139
+ * drag things into place is not a mistake.
140
+ *
141
+ * The four declared combinations are the ones that actually make a map
142
+ * illegible. Node/node hides an artefact outright; label/label and label/node
143
+ * make a name unreadable or attach it to the wrong thing; label/link is the one
144
+ * everybody hits, a name crossed out by the dependency running under it.
145
+ *
146
+ * Link/link is deliberately ABSENT: dependencies cross all the time on a real
147
+ * value chain, and that is the map working, not the map broken.
148
+ *
149
+ * ## Calibration (PO acceptance, 01/08/2026)
150
+ *
151
+ * Shipped, the rule was noisy in two ways the corpus could not see, because its
152
+ * fixtures were boxes rather than words:
153
+ *
154
+ * 1. a link crossing the EMPTY MARGIN of a label raised label/link. A label is
155
+ * created 120–200 units wide whatever it says, so a short name left-aligned
156
+ * in one leaves most of its box blank. Fixed where the mistake was: the
157
+ * label role is `kind: 'text'`, so the engine measures the ink and not the
158
+ * box (`textInkBound`).
159
+ * 2. two labels whose WORDS are nowhere near each other raised label/label,
160
+ * for the same reason and with the same fix.
161
+ *
162
+ * `minPenetration` is what remains: not every touch is a collision. **4 model
163
+ * units** — under a quarter of the 18-unit node, a sixth of the 26-unit label
164
+ * line, and about the width of a dependency's own stroke. A link grazing the
165
+ * top of a name and two names whose last and first letter share a hair are
166
+ * silent; a link through the middle of a name scores half its line height (13)
167
+ * and a name written across a node scores the height of the letters, so both
168
+ * are still reported with room to spare.
169
+ *
170
+ * It also absorbs the declared imprecision of the width approximation, which is
171
+ * on the same scale — measured against the real renderer over 28 names, the
172
+ * engine's per-character table reads between 11 % narrow and dead on, never
173
+ * wide — and points the same way, towards silence.
174
+ *
175
+ * IN RESERVE, if label/link is still noisy after this (PO, 01/08/2026): an
176
+ * ANGLE criterion — only a link crossing a name roughly PERPENDICULARLY strikes
177
+ * it out, one running along it at a shallow angle mostly runs beside it. It
178
+ * would be declared here as data, exactly like the threshold, and evaluated by
179
+ * the family. Not built: nobody has yet seen the noise it would remove.
180
+ */
181
+ const overlappingArtefacts = {
182
+ id: 'wardley.overlapping-artefacts',
183
+ framework: 'wardley',
184
+ family: 'no-overlap',
185
+ severity: 'warning',
186
+ // No `appliesTo`: this rule has no single subject role — the subjects are the
187
+ // pairs below, and naming one of them here would be data that lies.
188
+ roles: WARDLEY_ROLES,
189
+ messageKey: 'com.labre.wardley.validation.overlapping-artefacts',
190
+ messageFallback: 'These two overlap and make the map harder to read.',
191
+ suggestionKey: 'com.labre.wardley.validation.overlapping-artefacts.suggestion',
192
+ suggestionFallback: 'Move one of them aside.',
193
+ // 2: measured on the ink of a label rather than on its creation box, and
194
+ // calibrated with a penetration threshold — the same mistakes, fewer of the
195
+ // things that were never mistakes.
196
+ version: 2,
197
+ provenance: {
198
+ source: 'labre-convention',
199
+ reference: 'Labre readability convention — no Wardley mapping rule speaks of overlapping ink',
200
+ },
201
+ // Not a frame the rule measures against — an overlap is an overlap wherever
202
+ // it happens — but the map a finding is ATTRIBUTED to, so the arbitration
203
+ // "ignore this rule on the whole map" has one map to be written on.
204
+ backgroundRole: WARDLEY_ROLE.map,
205
+ overlap: [
206
+ [WARDLEY_ROLE.component, WARDLEY_ROLE.component],
207
+ [WARDLEY_ROLE.label, WARDLEY_ROLE.label],
208
+ [WARDLEY_ROLE.label, WARDLEY_ROLE.component],
209
+ [WARDLEY_ROLE.label, WARDLEY_ROLE.dependency],
210
+ ],
211
+ // How deep a collision has to be before it is one. See the header.
212
+ minPenetration: 4,
213
+ };
214
+ /**
215
+ * **W4** — a provider may not be positioned higher than its consumer.
216
+ *
217
+ * The value chain is the whole grammar of the map: the user sits at the top,
218
+ * each component rests on the components below it, and value flows back up. A
219
+ * dependency drawn from a component to something ABOVE it says the opposite of
220
+ * what the map means — either the link was drawn the wrong way round, or one of
221
+ * the two nodes is in the wrong place.
222
+ *
223
+ * ## The rule this one could not have been before `docs/adr/0010`
224
+ *
225
+ * W4 reads the persisted `source → target` pair of the edge, and that pair only
226
+ * became a STATEMENT the day the three mechanisms of that ADR landed: the link
227
+ * tool announces which way to drag (M1), a typed edge shows its orientation on
228
+ * hover and selection (M2), and the user can reverse it in one gesture (M3).
229
+ * Before them the direction was a by-product of which end the finger landed on
230
+ * first, and a rule on top of it would have spent the validation platform's
231
+ * credibility on its first false positive.
232
+ *
233
+ * The alternative — deriving the direction from the y coordinates — is not a
234
+ * cheaper W4, it is no W4 at all: the rule would compare the layout against
235
+ * itself and could never fire.
236
+ *
237
+ * ## The violation IS the affordance
238
+ *
239
+ * Nothing normalises the direction at creation, deliberately. An edge drawn
240
+ * upside-down raises this finding on the spot, and the user resolves it their
241
+ * way: drag the node, or reverse the relation. Either resolution is theirs.
242
+ *
243
+ * ## Tolerance
244
+ *
245
+ * 2% of the map's height, as a ratio and never as a number of units (the lesson
246
+ * of the 01/08/2026 recette, already learned by the equilibrium zone). Two
247
+ * components drawn level are not a mistake — a chain gets lined up before it
248
+ * gets spread out — so the rule only speaks when one is genuinely under the
249
+ * other. On the 900-high reference map that is 18 units, about the diameter of
250
+ * a component node.
251
+ */
252
+ const providerAboveConsumer = {
253
+ id: 'wardley.provider-above-consumer',
254
+ framework: 'wardley',
255
+ family: 'relative-order-along-axis',
256
+ severity: 'warning',
257
+ // No `appliesTo`: the subject of this rule is a RELATION, and the role that
258
+ // names it is declared where the family reads it — naming one of the three
259
+ // indicted elements here would be data that lies.
260
+ roles: WARDLEY_ROLES,
261
+ messageKey: 'com.labre.wardley.validation.provider-above-consumer',
262
+ messageFallback: 'This component sits above the one that depends on it.',
263
+ suggestionKey: 'com.labre.wardley.validation.provider-above-consumer.suggestion',
264
+ suggestionFallback: 'Needs run downwards on a Wardley map: move the provider below its consumer — or, if the link was drawn the wrong way round, reverse it.',
265
+ version: 1,
266
+ provenance: {
267
+ source: 'recommendation',
268
+ reference: 'Wardley mapping — the value-chain grammar of the map',
269
+ },
270
+ backgroundRole: WARDLEY_ROLE.map,
271
+ background: WARDLEY_BACKGROUND,
272
+ relativeOrder: {
273
+ edgeRole: WARDLEY_ROLE.dependency,
274
+ axis: 'value-chain',
275
+ // Tier 2 of ADR 0010: the verb of `wardley:dependency` is "depends on", so
276
+ // its source is the CONSUMER and sits higher on the visibility axis.
277
+ expect: 'source-ahead',
278
+ toleranceRatio: 0.02,
279
+ },
280
+ };
281
+ export const WARDLEY_RULES = [
282
+ changeArrowAgainstEvolution,
283
+ inertiaOffTransition,
284
+ overlappingArtefacts,
285
+ providerAboveConsumer,
286
+ ];