@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.
- package/dist/actions.d.ts +63 -20
- package/dist/actions.js +190 -57
- package/dist/audit-criteria.d.ts +31 -0
- package/dist/audit-criteria.js +90 -0
- package/dist/background.d.ts +14 -0
- package/dist/background.js +338 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +352 -0
- package/dist/consts.d.ts +10 -4
- package/dist/consts.js +10 -9
- package/dist/descriptor.d.ts +8 -3
- package/dist/descriptor.js +6 -3
- package/dist/element-renderer.d.ts +8 -5
- package/dist/element-renderer.js +12 -147
- package/dist/element-view.d.ts +8 -4
- package/dist/element-view.js +30 -22
- package/dist/export.d.ts +211 -0
- package/dist/export.js +655 -0
- package/dist/gradient.d.ts +6 -11
- package/dist/gradient.js +59 -48
- package/dist/import.d.ts +116 -0
- package/dist/import.js +905 -0
- package/dist/index.d.ts +27 -1
- package/dist/index.js +31 -1
- package/dist/interchange.d.ts +80 -0
- package/dist/interchange.js +138 -0
- package/dist/legend.js +8 -0
- package/dist/natures.d.ts +50 -0
- package/dist/natures.js +93 -0
- package/dist/node/node-renderer.js +1 -1
- package/dist/nudges.d.ts +41 -0
- package/dist/nudges.js +69 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +87 -0
- package/dist/reading.d.ts +3 -0
- package/dist/reading.js +129 -0
- package/dist/roles.d.ts +50 -0
- package/dist/roles.js +132 -0
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +286 -0
- package/dist/templates/index.js +66 -10
- package/dist/templates/maps.js +146 -22
- package/dist/toolbar/config.js +3 -1
- package/dist/toolbar/icons.d.ts +20 -0
- package/dist/toolbar/icons.js +34 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/toolbar/wardley-menu.d.ts +8 -15
- package/dist/toolbar/wardley-menu.js +8 -136
- package/dist/toolbar/wardley-senior-button.js +12 -6
- package/dist/translations.d.ts +16 -0
- package/dist/translations.js +24 -0
- package/dist/view.d.ts +17 -0
- package/dist/view.js +127 -13
- package/package.json +2 -2
- package/dist/label-layout.d.ts +0 -20
- package/dist/label-layout.js +0 -72
- package/dist/shortcuts.d.ts +0 -2
- package/dist/shortcuts.js +0 -37
package/dist/reading.js
ADDED
|
@@ -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
|
+
};
|
package/dist/roles.d.ts
ADDED
|
@@ -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])));
|
package/dist/rules.d.ts
ADDED
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
|
+
];
|