@formicoidea/labre-framework-edgy 0.32.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/roles.js ADDED
@@ -0,0 +1,182 @@
1
+ import { EDGY_DYNAMIC_NODES, EDGY_DYNAMIC_RELATIONS, edgyElementLabel, } from './metamodel.js';
2
+ import { NODE_LABEL } from './node/consts.js';
3
+ /** Role ids, keyed by the name used at the creation sites. */
4
+ export const EDGY_ROLE = {
5
+ element: 'edgy:element',
6
+ // The four persisted kinds (`EdgyNodeKind`).
7
+ people: 'edgy:people',
8
+ outcome: 'edgy:outcome',
9
+ object: 'edgy:object',
10
+ activity: 'edgy:activity',
11
+ // The twelve official elements.
12
+ purpose: 'edgy:purpose',
13
+ capability: 'edgy:capability',
14
+ task: 'edgy:task',
15
+ story: 'edgy:story',
16
+ process: 'edgy:process',
17
+ journey: 'edgy:journey',
18
+ content: 'edgy:content',
19
+ asset: 'edgy:asset',
20
+ channel: 'edgy:channel',
21
+ organisation: 'edgy:organisation',
22
+ product: 'edgy:product',
23
+ brand: 'edgy:brand',
24
+ // The two frames.
25
+ background: 'edgy:background',
26
+ facets: 'edgy:facets',
27
+ board: 'edgy:board',
28
+ // The parent of the 22 verbs.
29
+ relation: 'edgy:relation',
30
+ };
31
+ /**
32
+ * A canonical verb, as a role id: `is part of` → `edgy:is-part-of`.
33
+ *
34
+ * The verb IS the identifier, so a relation added to the metamodel gets its
35
+ * role for free and nobody has to remember to name it. No collision with the
36
+ * element roles above is possible in the metamodel as it stands, and a future
37
+ * verb colliding with an element name would be caught by the unit spec, which
38
+ * counts the vocabulary.
39
+ */
40
+ function verbRoleId(verb) {
41
+ return `edgy:${verb.replace(/\s+/g, '-')}`;
42
+ }
43
+ /**
44
+ * Role id per canonical verb, DERIVED from {@link EDGY_DYNAMIC_RELATIONS}.
45
+ *
46
+ * Derived rather than restated: the metamodel is written once (`./metamodel.ts`)
47
+ * and the template that draws it, the vocabulary that names it and the rule that
48
+ * checks it all read the same table. `requires` appears on three rows and gets
49
+ * ONE role — a verb is a verb wherever it is spoken, and the three sentences it
50
+ * belongs to are three triplets, not three roles.
51
+ */
52
+ export const EDGY_VERB_ROLE = Object.assign(Object.create(null), Object.fromEntries(EDGY_DYNAMIC_RELATIONS.map(([, , verb]) => [verb, verbRoleId(verb)])));
53
+ /**
54
+ * Key of an ORDERED pair of element roles: `edgy:process edgy:capability`.
55
+ *
56
+ * A SPACE separates them, and not `-`, `:` or `>`: a role id already contains a
57
+ * colon and may contain a dash (`edgy:is-part-of`), and a separator that can
58
+ * occur inside a value is a lookup table with collisions waiting in it. No role
59
+ * id has ever contained whitespace — `verbRoleId` above replaces it on the one
60
+ * family that could.
61
+ */
62
+ export const edgyPairKey = (source, target) => `${source} ${target}`;
63
+ /**
64
+ * Ordered pair of ELEMENT roles → the canonical verb the metamodel gives it,
65
+ * DERIVED from {@link EDGY_DYNAMIC_RELATIONS}.
66
+ *
67
+ * The metamodel's 24 rows are 24 DISTINCT ordered pairs, so the verb of a
68
+ * relation is entirely determined by which two elements it runs between: there
69
+ * is never a choice to offer. That is what lets the toolbox ship ONE "Relation"
70
+ * entry instead of twenty-two, and what `./relation.ts` reads to name a
71
+ * hand-drawn link after the pair the user attached it to.
72
+ *
73
+ * Derived, never restated — same contract as {@link EDGY_VERB_ROLE}: a relation
74
+ * added to `./metamodel.ts` becomes nameable by hand without anybody editing
75
+ * this file.
76
+ */
77
+ export const EDGY_PAIR_TO_VERB = Object.assign(Object.create(null), Object.fromEntries(EDGY_DYNAMIC_RELATIONS.map(([source, target, verb]) => [
78
+ edgyPairKey(EDGY_ROLE[source], EDGY_ROLE[target]),
79
+ verb,
80
+ ])));
81
+ /** i18n key stem of a role id: `edgy:is-part-of` → `com.labre.edgy.role.is-part-of`. */
82
+ const roleKey = (id) => `com.labre.edgy.role.${id.slice('edgy:'.length)}`;
83
+ const ELEMENT_DEFS = [
84
+ // The root of every artefact a board is made of. A rule written here covers
85
+ // the four kinds and the twelve elements at once — which is exactly what
86
+ // `edgy.overlapping-artefacts` needs and why the root exists.
87
+ {
88
+ id: EDGY_ROLE.element,
89
+ kind: 'node',
90
+ labelKey: roleKey(EDGY_ROLE.element),
91
+ labelFallback: 'Element',
92
+ },
93
+ // The four PERSISTED kinds. They are the base shapes the toolbox offers, so
94
+ // they are what an element created from the palette carries: somebody
95
+ // dropping an "Object" on the board has said "object" and nothing more, and
96
+ // the role says exactly that much.
97
+ //
98
+ // The fallback is the wording the palette itself writes inside the shape
99
+ // (`node/consts.ts`), so a reader who meets the role in a legend or a
100
+ // direction reveal meets the word they dropped on the board.
101
+ ...['people', 'outcome', 'object', 'activity'].map(kind => ({
102
+ id: EDGY_ROLE[kind],
103
+ parent: EDGY_ROLE.element,
104
+ kind: 'node',
105
+ labelKey: roleKey(EDGY_ROLE[kind]),
106
+ labelFallback: NODE_LABEL[kind],
107
+ })),
108
+ // The twelve official elements, each under the kind the metamodel draws it
109
+ // with — Purpose is an outcome, Story an activity, Channel an object. The
110
+ // parent is READ from `EDGY_DYNAMIC_NODES` rather than restated: the diagram
111
+ // and the vocabulary cannot drift apart if there is only one table. So is the
112
+ // fallback wording, from the same table's own names.
113
+ ...Object.entries(EDGY_DYNAMIC_NODES).map(([name, { kind }]) => ({
114
+ id: EDGY_ROLE[name],
115
+ parent: EDGY_ROLE[kind],
116
+ kind: 'node',
117
+ labelKey: roleKey(EDGY_ROLE[name]),
118
+ labelFallback: edgyElementLabel(name),
119
+ })),
120
+ ];
121
+ const BACKGROUND_DEFS = [
122
+ // The frame, and deliberately NOT a child of `edgy:element`: a rule written
123
+ // on the artefacts must never match the board they are drawn on. Two frames
124
+ // specialise it — the facets diagram and the blank board — so a rule
125
+ // attributes its findings to whichever one the user is working on without
126
+ // naming either.
127
+ {
128
+ id: EDGY_ROLE.background,
129
+ kind: 'node',
130
+ labelKey: roleKey(EDGY_ROLE.background),
131
+ },
132
+ {
133
+ id: EDGY_ROLE.facets,
134
+ parent: EDGY_ROLE.background,
135
+ kind: 'node',
136
+ labelKey: roleKey(EDGY_ROLE.facets),
137
+ },
138
+ {
139
+ id: EDGY_ROLE.board,
140
+ parent: EDGY_ROLE.background,
141
+ kind: 'node',
142
+ labelKey: roleKey(EDGY_ROLE.board),
143
+ },
144
+ ];
145
+ /**
146
+ * The relations: the parent, then one child per canonical verb.
147
+ *
148
+ * The parent carries no `direction` — it names no verb, so it has no sentence
149
+ * to state. Every child does, and the gesture hint is derived from the verb for
150
+ * the same reason the id is: "drag from the X to the Y" is the same sentence
151
+ * with the same two holes for all 22 of them, and writing it out 22 times would
152
+ * be 22 chances to write it out wrong.
153
+ */
154
+ const RELATION_DEFS = [
155
+ {
156
+ id: EDGY_ROLE.relation,
157
+ kind: 'edge',
158
+ labelKey: roleKey(EDGY_ROLE.relation),
159
+ labelFallback: 'Relation',
160
+ },
161
+ ...Object.entries(EDGY_VERB_ROLE).map(([verb, id]) => ({
162
+ id,
163
+ parent: EDGY_ROLE.relation,
164
+ kind: 'edge',
165
+ labelKey: roleKey(id),
166
+ labelFallback: verb.charAt(0).toUpperCase() + verb.slice(1),
167
+ direction: {
168
+ verbKey: `${roleKey(id)}.verb`,
169
+ verbFallback: verb,
170
+ gestureHintKey: `${roleKey(id)}.gesture`,
171
+ gestureHintFallback: `Drag from the element that is the subject of "${verb}" to its object.`,
172
+ },
173
+ })),
174
+ ];
175
+ const DEFS = [
176
+ ...ELEMENT_DEFS,
177
+ ...BACKGROUND_DEFS,
178
+ ...RELATION_DEFS,
179
+ ];
180
+ // Null prototype: this is a lookup table keyed by ids that may one day come
181
+ // from host-supplied packs, so `defs['toString']` must not resolve.
182
+ export const EDGY_ROLES = Object.assign(Object.create(null), Object.fromEntries(DEFS.map(def => [def.id, def])));
@@ -0,0 +1,43 @@
1
+ import type { EndpointTriplet, ValidationRule } from '@formicoidea/labre-core/blocks/surface';
2
+ /**
3
+ * EDGY validation rules (WS1).
4
+ *
5
+ * DATA owned by the framework, versioned per rule: the engine
6
+ * (`@labre/affine-block-surface`) knows how to evaluate a FAMILY, never a
7
+ * concrete rule. Adding an EDGY rule is adding an entry to this array.
8
+ *
9
+ * Registered from the flag-gated `EdgyViewExtension`, so switching the EDGY
10
+ * flag off removes the rules with the rest of the tooling — boards already
11
+ * drawn keep rendering, they simply stop being checked (`docs/adr/0009`).
12
+ *
13
+ * ## Two rules, and the ones deliberately left out
14
+ *
15
+ * The PO arbitration of 26/08/2026 put EDGY's two JUDGEMENT controls — "every
16
+ * intersection element is linked to both its parent facets" and "every element
17
+ * wears its facet's colour" — in `./nudges.ts` rather than here. Neither is
18
+ * decidable: the first would have to guess which circle an element belongs to
19
+ * from where somebody dropped it, and the second would indict a board whose
20
+ * author uses their own palette. A checklist can ask them; an algorithm cannot
21
+ * answer them.
22
+ *
23
+ * ## Debt: `tree-mixed-kinds`
24
+ *
25
+ * EDGY's own notation has TREES — a capability tree, an organisation tree —
26
+ * whose levels must all be the same kind of element. There is no tree on this
27
+ * canvas yet: nothing draws one, nothing stores one, and no role names one. The
28
+ * rule is therefore deferred rather than written against an artefact that does
29
+ * not exist. When a tree lands, it comes with its own role and this is where its
30
+ * rule goes.
31
+ */
32
+ /**
33
+ * The 24 sanctioned sentences, DERIVED from the metamodel.
34
+ *
35
+ * One triplet per row of `EDGY_DYNAMIC_RELATIONS`, read exactly as
36
+ * `docs/adr/0010` reads a typed edge: source is the subject, target the object.
37
+ * The matrix is never restated — a relation added to the metamodel is drawn by
38
+ * the template, gets a role from `roles.ts` and becomes legal here, all from the
39
+ * one table. `requires` contributes three triplets under one role, which is what
40
+ * it means for a verb to be spoken of three different pairs.
41
+ */
42
+ export declare const EDGY_ALLOWED_RELATIONS: readonly EndpointTriplet[];
43
+ export declare const EDGY_RULES: readonly ValidationRule[];
package/dist/rules.js ADDED
@@ -0,0 +1,153 @@
1
+ import { EDGY_DYNAMIC_RELATIONS } from './metamodel.js';
2
+ import { EDGY_ROLE, EDGY_ROLES, EDGY_VERB_ROLE, } from './roles.js';
3
+ /**
4
+ * EDGY validation rules (WS1).
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 an EDGY rule is adding an entry to this array.
9
+ *
10
+ * Registered from the flag-gated `EdgyViewExtension`, so switching the EDGY
11
+ * flag off removes the rules with the rest of the tooling — boards already
12
+ * drawn keep rendering, they simply stop being checked (`docs/adr/0009`).
13
+ *
14
+ * ## Two rules, and the ones deliberately left out
15
+ *
16
+ * The PO arbitration of 26/08/2026 put EDGY's two JUDGEMENT controls — "every
17
+ * intersection element is linked to both its parent facets" and "every element
18
+ * wears its facet's colour" — in `./nudges.ts` rather than here. Neither is
19
+ * decidable: the first would have to guess which circle an element belongs to
20
+ * from where somebody dropped it, and the second would indict a board whose
21
+ * author uses their own palette. A checklist can ask them; an algorithm cannot
22
+ * answer them.
23
+ *
24
+ * ## Debt: `tree-mixed-kinds`
25
+ *
26
+ * EDGY's own notation has TREES — a capability tree, an organisation tree —
27
+ * whose levels must all be the same kind of element. There is no tree on this
28
+ * canvas yet: nothing draws one, nothing stores one, and no role names one. The
29
+ * rule is therefore deferred rather than written against an artefact that does
30
+ * not exist. When a tree lands, it comes with its own role and this is where its
31
+ * rule goes.
32
+ */
33
+ /**
34
+ * The 24 sanctioned sentences, DERIVED from the metamodel.
35
+ *
36
+ * One triplet per row of `EDGY_DYNAMIC_RELATIONS`, read exactly as
37
+ * `docs/adr/0010` reads a typed edge: source is the subject, target the object.
38
+ * The matrix is never restated — a relation added to the metamodel is drawn by
39
+ * the template, gets a role from `roles.ts` and becomes legal here, all from the
40
+ * one table. `requires` contributes three triplets under one role, which is what
41
+ * it means for a verb to be spoken of three different pairs.
42
+ */
43
+ export const EDGY_ALLOWED_RELATIONS = EDGY_DYNAMIC_RELATIONS.map(([source, target, verb]) => ({
44
+ source: EDGY_ROLE[source],
45
+ edge: EDGY_VERB_ROLE[verb],
46
+ target: EDGY_ROLE[target],
47
+ }));
48
+ /**
49
+ * **E1** — a relation says something the EDGY metamodel does not.
50
+ *
51
+ * EDGY is a language before it is a diagram: its 24 relations are the sentences
52
+ * the method sanctions, and a link drawn between the wrong two elements — or
53
+ * drawn the right way round between the right two but carrying the wrong verb —
54
+ * is a modelling mistake no amount of dragging fixes. "A journey traverses a
55
+ * channel" is EDGY; "a channel traverses a journey" is a sentence nobody can
56
+ * act on.
57
+ *
58
+ * ## What stays silent
59
+ *
60
+ * Everything the `relation-endpoints` family already keeps quiet about, and one
61
+ * of them matters more here than anywhere: **a plain connector carries no role
62
+ * and is never judged**. Free links are how an EDGY workshop actually runs —
63
+ * somebody draws an arrow to say "these two have something to do with each
64
+ * other" and names it later — and the day the tool starts indicting that
65
+ * gesture is the day it gets switched off. Only the typed relations the toolbox
66
+ * and the templates stamp are read, and only when BOTH ends are official
67
+ * elements: a relation onto a bare sticky, onto a People node or onto an
68
+ * element of another framework is outside the alphabet, hence outside the
69
+ * conversation.
70
+ *
71
+ * Severity `warning` and not `blocking-overridable`: nothing in this library
72
+ * refuses a gesture, so declaring the blocking level would be data claiming an
73
+ * effect that does not exist (the note `wardley/rules.ts` carries at length).
74
+ * The profiles demote it to `audit` on a sketch.
75
+ */
76
+ const nonCanonicalLink = {
77
+ id: 'edgy.non-canonical-link',
78
+ framework: 'edgy',
79
+ family: 'relation-endpoints',
80
+ severity: 'warning',
81
+ // No `appliesTo`: the subject is a RELATION, and the role that names it is
82
+ // declared where the family reads it — naming one of the three indicted
83
+ // elements here would be data that lies.
84
+ roles: EDGY_ROLES,
85
+ messageKey: 'com.labre.edgy.validation.non-canonical-link',
86
+ messageFallback: 'This relation is not one the EDGY metamodel declares between these two elements.',
87
+ suggestionKey: 'com.labre.edgy.validation.non-canonical-link.suggestion',
88
+ suggestionFallback: 'Read the link out loud — source, verb, target. Reverse it, re-point an end, or use the verb EDGY gives these two elements.',
89
+ version: 1,
90
+ provenance: {
91
+ source: 'recommendation',
92
+ reference: 'EDGY (Enterprise Design) — the 24 relations its metamodel sanctions',
93
+ },
94
+ // Not a frame the rule measures against — a sentence is right or wrong
95
+ // wherever it is written — but the facets diagram or board a finding is
96
+ // ATTRIBUTED to, so the arbitration "ignore this rule on the whole board" has
97
+ // somewhere to live (the `no-overlap` pattern).
98
+ backgroundRole: EDGY_ROLE.background,
99
+ endpoints: {
100
+ // The PARENT role: every verb specialises it, so the rule reads all 22 of
101
+ // them and no free connector.
102
+ edgeRole: EDGY_ROLE.relation,
103
+ allowed: EDGY_ALLOWED_RELATIONS,
104
+ },
105
+ };
106
+ /**
107
+ * **E2** — two artefacts must not sit on top of each other.
108
+ *
109
+ * A readability rule, the same one Wardley ships and for the same reason: a
110
+ * board you cannot read is useless, while a momentary overlap during a drag is
111
+ * not a mistake. It matters more on an EDGY facets diagram than almost
112
+ * anywhere, because WHERE an element sits is what says which facet it belongs
113
+ * to: an artefact hidden under another has lost its meaning, not just its
114
+ * legibility.
115
+ *
116
+ * ONE declared pair, `element × element`, because `roleIsA` covers the whole
117
+ * subtree — the four kinds and the twelve official elements — from the root.
118
+ * Backgrounds are outside that subtree by construction, so an element sitting on
119
+ * the diagram it belongs to is never a collision.
120
+ *
121
+ * `minPenetration: 4` model units, the calibration Wardley's W3 arrived at on
122
+ * the recette of 01/08/2026: under a twentieth of an EDGY node's 80-unit height
123
+ * and about twice a connector's stroke, so two boxes whose corners share a hair
124
+ * are silent while a box genuinely covering another is not. Verified against
125
+ * every layout this package ships: the twelve nodes of the EDGY dynamic
126
+ * template — the only stamped layout, hence the only one this rule can see —
127
+ * clear each other by 80 model units at the closest, twenty times the
128
+ * threshold.
129
+ */
130
+ const overlappingArtefacts = {
131
+ id: 'edgy.overlapping-artefacts',
132
+ framework: 'edgy',
133
+ family: 'no-overlap',
134
+ severity: 'warning',
135
+ // No `appliesTo`: the subjects of a `no-overlap` rule are the pairs below.
136
+ roles: EDGY_ROLES,
137
+ messageKey: 'com.labre.edgy.validation.overlapping-artefacts',
138
+ messageFallback: 'These two overlap and make the board harder to read.',
139
+ suggestionKey: 'com.labre.edgy.validation.overlapping-artefacts.suggestion',
140
+ suggestionFallback: 'Move one of them aside — on a facets diagram, where an element sits is what says which facet it belongs to.',
141
+ version: 1,
142
+ provenance: {
143
+ source: 'labre-convention',
144
+ reference: 'Labre readability convention — no EDGY rule speaks of overlapping ink',
145
+ },
146
+ backgroundRole: EDGY_ROLE.background,
147
+ overlap: [[EDGY_ROLE.element, EDGY_ROLE.element]],
148
+ minPenetration: 4,
149
+ };
150
+ export const EDGY_RULES = [
151
+ nonCanonicalLink,
152
+ overlappingArtefacts,
153
+ ];
@@ -6,20 +6,12 @@ import { type Template, type TemplateCategory } from '@formicoidea/labre-core/gf
6
6
  */
7
7
  export declare const DYN_SCALE = 4.8;
8
8
  /**
9
- * The 24 canonical EDGY relations (source, target, verb, label position along
10
- * the link) — exported for the unit tests. 7 per facet + 3 between the
11
- * intersections. The optional 4th member mirrors the reference diagram's
12
- * placements: verbs of intersection-outgoing links sit near the far element
13
- * (`labelOffset.distance` ≈ .75), short peer links keep the middle.
9
+ * The metamodel itself the 12 elements and the 24 relations — now lives in
10
+ * `../metamodel.ts`, so the role vocabulary can DERIVE from it without this
11
+ * module and that one importing each other. Re-exported here under the names
12
+ * they have always had: nothing that reads them had to change.
14
13
  */
15
- export declare const EDGY_DYNAMIC_RELATIONS: [string, string, string, number?][];
16
- export declare const EDGY_DYNAMIC_NODES: Record<string, {
17
- kind: 'outcome' | 'object' | 'activity';
18
- cx: number;
19
- cy: number;
20
- w?: number;
21
- fill: string;
22
- }>;
14
+ export { EDGY_DYNAMIC_NODES, EDGY_DYNAMIC_RELATIONS, type EdgyElementName, } from '../metamodel.js';
23
15
  /**
24
16
  * Reference coords → template model coords: the background element sits at
25
17
  * (0,0) and renders the cropped circles box, so a reference point maps to