@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/actions.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
2
+ import { type GfxController } from '@formicoidea/labre-core/std/gfx';
2
3
  export type EdgyBoxKind = 'outcome' | 'object' | 'activity';
3
4
  /**
4
5
  * Create the Enterprise Design Facets diagram centred on the viewport —
@@ -19,3 +20,20 @@ export declare function createEdgyBox(std: BlockStdScope, kind: EdgyBoxKind): vo
19
20
  * person glyph by the renderer, plus a native text label below, grouped.
20
21
  */
21
22
  export declare function createEdgyPeople(std: BlockStdScope): void;
23
+ /**
24
+ * Activate the native connector tool for an EDGY RELATION: pre-styled like the
25
+ * 24 links of the metamodel template, and stamped with the GENERIC role
26
+ * `edgy:relation`.
27
+ *
28
+ * Generic on purpose, and this is the whole design (`./relation.ts`): the verb
29
+ * of an EDGY relation is determined by the ordered pair of elements it runs
30
+ * between — 24 rows, 24 distinct pairs — so there is nothing to ask the user
31
+ * and nothing to pick from a list of twenty-two. The tool arms the parent role,
32
+ * the user drags from the subject to the object, and `EdgyRelationResolver`
33
+ * writes the verb the metamodel gives that pair, as a role and as the label.
34
+ *
35
+ * The style is the template's, to the pixel: straight, `NODE_STROKE`, two units
36
+ * wide, and NO arrowhead at either end — EDGY's reference diagram draws its
37
+ * relations as bare lines and lets the verb say which way the sentence runs.
38
+ */
39
+ export declare function activateEdgyRelation(gfx: GfxController): void;
package/dist/actions.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
2
3
  import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
3
4
  import { createTemplateJob } from '@formicoidea/labre-core/gfx/template';
4
- import { FontFamily, ShapeStyle } from '@formicoidea/labre-core/model';
5
+ import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
5
6
  import { Bound } from '@formicoidea/labre-core/global/gfx';
6
7
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
7
8
  import { CROP_LABELED } from './consts.js';
8
9
  import { ACTIVITY_VERTICES, INNER_FONT_SIZE, LABEL_FONT_SIZE, LABEL_GAP, NODE_FILL, NODE_LABEL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, OUTCOME_RADIUS, } from './node/consts.js';
10
+ import { EDGY_ROLE } from './roles.js';
9
11
  import { edgyDynamicTemplate } from './templates/index.js';
12
+ /** Stroke width the 24 template relations are drawn with. */
13
+ const RELATION_STROKE_WIDTH = 2;
10
14
  /** Default facets-diagram size (REF aspect, scaled up so it reads on canvas). */
11
15
  const FACETS_SCALE = 1.5;
12
16
  /** Default blank-board size. */
@@ -21,10 +25,20 @@ function finish(gfx, id) {
21
25
  gfx.selection.set({ elements: [id], editing: false });
22
26
  // Keep the palette open (native sub-menu behaviour).
23
27
  }
24
- /** Shared props for an EDGY node shape. */
28
+ /**
29
+ * Shared props for an EDGY node shape.
30
+ *
31
+ * The `role` is the PERSISTED KIND and nothing more: somebody picking "Object"
32
+ * in the palette has said "this is an object", not "this is a Channel". The
33
+ * twelve official elements are named by the metamodel template, which knows
34
+ * which of them it is drawing; a base element created here specialises
35
+ * `edgy:element` through its kind and is judged by every rule written on the
36
+ * root — the overlap rule — and by none written on a leaf.
37
+ */
25
38
  const baseShapeProps = (kind) => ({
26
39
  type: 'edgyNode',
27
40
  kind,
41
+ role: EDGY_ROLE[kind],
28
42
  filled: true,
29
43
  fillColor: NODE_FILL,
30
44
  strokeColor: NODE_STROKE,
@@ -57,6 +71,9 @@ export function createEdgyFacets(std) {
57
71
  const id = gfx.surface.addElement({
58
72
  type: 'edgy',
59
73
  cropToCircles: true,
74
+ // The frame: what makes this an EDGY board rather than three circles, and
75
+ // what a finding is attributed to.
76
+ role: EDGY_ROLE.facets,
60
77
  xywh: new Bound(centerX - width / 2, centerY - height / 2, width, height).serialize(),
61
78
  });
62
79
  finish(gfx, id);
@@ -86,6 +103,9 @@ export function createEdgyBoard(std) {
86
103
  const { centerX, centerY } = gfx.viewport;
87
104
  const id = gfx.surface.addElement({
88
105
  type: 'edgyBoard',
106
+ // The blank board is a frame too: the same rules apply to what is drawn on
107
+ // it, and the same map-wide arbitration has to have somewhere to live.
108
+ role: EDGY_ROLE.board,
89
109
  xywh: new Bound(centerX - BOARD_W / 2, centerY - BOARD_H / 2, BOARD_W, BOARD_H).serialize(),
90
110
  });
91
111
  finish(gfx, id);
@@ -138,3 +158,36 @@ export function createEdgyPeople(std) {
138
158
  });
139
159
  finish(gfx, result.groupId || nodeId);
140
160
  }
161
+ /**
162
+ * Activate the native connector tool for an EDGY RELATION: pre-styled like the
163
+ * 24 links of the metamodel template, and stamped with the GENERIC role
164
+ * `edgy:relation`.
165
+ *
166
+ * Generic on purpose, and this is the whole design (`./relation.ts`): the verb
167
+ * of an EDGY relation is determined by the ordered pair of elements it runs
168
+ * between — 24 rows, 24 distinct pairs — so there is nothing to ask the user
169
+ * and nothing to pick from a list of twenty-two. The tool arms the parent role,
170
+ * the user drags from the subject to the object, and `EdgyRelationResolver`
171
+ * writes the verb the metamodel gives that pair, as a role and as the label.
172
+ *
173
+ * The style is the template's, to the pixel: straight, `NODE_STROKE`, two units
174
+ * wide, and NO arrowhead at either end — EDGY's reference diagram draws its
175
+ * relations as bare lines and lets the verb say which way the sentence runs.
176
+ */
177
+ export function activateEdgyRelation(gfx) {
178
+ gfx.tool.setTool(ConnectorTool, {
179
+ mode: ConnectorMode.Straight,
180
+ role: EDGY_ROLE.relation,
181
+ // The look rides on the activation, never through the last-props store:
182
+ // the plain connector tool must keep the user's own style (#144 M1).
183
+ style: {
184
+ stroke: NODE_STROKE,
185
+ strokeStyle: StrokeStyle.Solid,
186
+ strokeWidth: RELATION_STROKE_WIDTH,
187
+ frontEndpointStyle: PointStyle.None,
188
+ rearEndpointStyle: PointStyle.None,
189
+ },
190
+ });
191
+ // Keep the palette open (native sub-menu behaviour): it only closes on
192
+ // re-click of the senior button, another senior tool, or Escape.
193
+ }
@@ -13,7 +13,10 @@ export const edgyBoard = (model, ctx, matrix) => {
13
13
  const [, , w, h] = model.deserializedXYWH;
14
14
  const cx = w / 2;
15
15
  const cy = h / 2;
16
- ctx.setTransform(matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy));
16
+ ctx.setTransform(matrix
17
+ .translateSelf(cx, cy)
18
+ .rotateSelf(model.rotate)
19
+ .translateSelf(-cx, -cy));
17
20
  ctx.beginPath();
18
21
  ctx.roundRect(0, 0, w, h, BOARD_RADIUS);
19
22
  ctx.fillStyle = BOARD_FILL;
package/dist/commands.js CHANGED
@@ -1,5 +1,6 @@
1
- import { createEdgyBoard, createEdgyBox, createEdgyDynamic, createEdgyFacets, createEdgyPeople, } from './actions.js';
2
- import { edgyActivityIcon, edgyBoardIcon, edgyDynamicIcon, edgyFacetsIcon, edgyObjectIcon, edgyOutcomeIcon, edgyPeopleIcon, } from './toolbar/icons.js';
1
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
2
+ import { activateEdgyRelation, createEdgyBoard, createEdgyBox, createEdgyDynamic, createEdgyFacets, createEdgyPeople, } from './actions.js';
3
+ import { edgyActivityIcon, edgyBoardIcon, edgyDynamicIcon, edgyFacetsIcon, edgyObjectIcon, edgyOutcomeIcon, edgyPeopleIcon, edgyRelationIcon, } from './toolbar/icons.js';
3
4
  const SPECS = [
4
5
  {
5
6
  id: 'addFacets',
@@ -57,13 +58,29 @@ const SPECS = [
57
58
  element: 'node:activity',
58
59
  run: std => createEdgyBox(std, 'activity'),
59
60
  },
61
+ {
62
+ id: 'addRelation',
63
+ label: 'Relation',
64
+ description: 'Drag from the element that is the subject of the relation to the one it is about; EDGY names the link itself.',
65
+ iconKey: 'edgy.relation',
66
+ category: 'relations',
67
+ kind: 'tool',
68
+ element: 'connector:relation',
69
+ run: std => activateEdgyRelation(std.get(GfxControllerIdentifier)),
70
+ },
60
71
  ];
61
72
  export const edgyCommands = SPECS.map((spec, order) => ({
62
73
  id: `edgy.${spec.id}`,
63
74
  owner: 'edgy',
64
- kind: 'artefact',
75
+ kind: spec.kind ?? 'artefact',
65
76
  labelKey: `com.labre.commands.edgy.${spec.id}`,
66
77
  labelFallback: spec.label,
78
+ ...(spec.description === undefined
79
+ ? {}
80
+ : {
81
+ descriptionKey: `com.labre.commands.edgy.${spec.id}.description`,
82
+ descriptionFallback: spec.description,
83
+ }),
67
84
  category: spec.category,
68
85
  iconKey: spec.iconKey,
69
86
  surfaces: ['senior-menu', 'catalogue', 'palette', 'agent'],
@@ -82,4 +99,5 @@ export const edgyCommandIcons = {
82
99
  'edgy.outcome': edgyOutcomeIcon,
83
100
  'edgy.object': edgyObjectIcon,
84
101
  'edgy.activity': edgyActivityIcon,
102
+ 'edgy.relation': edgyRelationIcon,
85
103
  };
@@ -1,6 +1,6 @@
1
1
  import { ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { COLORS, cropLabeledScale, cropScale, FONT_FAMILY, LABEL_FONT_SIZE, PICTO_STROKE, REF_H, REF_W, refScale, VENN, } from './consts.js';
3
- import { CIRCLE_A, CIRCLE_B, CIRCLE_C, facetLabelAnchors } from './label-layout.js';
3
+ import { CIRCLE_A, CIRCLE_B, CIRCLE_C, facetLabelAnchors, } from './label-layout.js';
4
4
  /**
5
5
  * Canvas renderer for the EDGY Enterprise Design Facets diagram — reproduces the
6
6
  * validated mockup: three overlapping circles (Identity / Architecture /
@@ -15,7 +15,10 @@ export const edgy = (model, ctx, matrix) => {
15
15
  const [, , w, h] = model.deserializedXYWH;
16
16
  const cx = w / 2;
17
17
  const cy = h / 2;
18
- ctx.setTransform(matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy));
18
+ ctx.setTransform(matrix
19
+ .translateSelf(cx, cy)
20
+ .rotateSelf(model.rotate)
21
+ .translateSelf(-cx, -cy));
19
22
  // Uniform fit of the reference design, centered (letterboxed) — or, when
20
23
  // the diagram is cropped, of the circles' bounding box (plus the facet
21
24
  // label allowance if the labels are shown).
@@ -1,7 +1,13 @@
1
1
  import { LABEL_FONT_SIZE, VENN } from './consts.js';
2
2
  /** The three circle centres in reference coords. */
3
- export const CIRCLE_A = { x: VENN.cx - 0.866 * VENN.r0, y: VENN.cy - 0.5 * VENN.r0 }; // Identity
4
- export const CIRCLE_B = { x: VENN.cx + 0.866 * VENN.r0, y: VENN.cy - 0.5 * VENN.r0 }; // Architecture
3
+ export const CIRCLE_A = {
4
+ x: VENN.cx - 0.866 * VENN.r0,
5
+ y: VENN.cy - 0.5 * VENN.r0,
6
+ }; // Identity
7
+ export const CIRCLE_B = {
8
+ x: VENN.cx + 0.866 * VENN.r0,
9
+ y: VENN.cy - 0.5 * VENN.r0,
10
+ }; // Architecture
5
11
  export const CIRCLE_C = { x: VENN.cx, y: VENN.cy + VENN.r0 }; // Experience
6
12
  /**
7
13
  * The three facet name anchors in reference coords, positioned fully outside
@@ -0,0 +1,2 @@
1
+ import { type AutoLegendSpec } from '@formicoidea/labre-ddd-shared';
2
+ export declare const EDGY_AUTO_LEGEND: AutoLegendSpec;
package/dist/legend.js ADDED
@@ -0,0 +1,109 @@
1
+ import { roleLabel, } from '@formicoidea/labre-ddd-shared';
2
+ import { EDGY_DYNAMIC_NODES, EDGY_ZONE_FILL, EDGY_ZONES, edgyElementLabel, } from './metamodel.js';
3
+ import { NODE_FILL, NODE_STROKE } from './node/consts.js';
4
+ import { EDGY_ROLE, EDGY_ROLES } from './roles.js';
5
+ /**
6
+ * What an EDGY background's automatic legend can say — a TABLE, and nothing
7
+ * else: the scan, the placement and the box are `createAutoLegend`'s job, the
8
+ * same generic the three DDD backgrounds were given
9
+ * (`@labre/affine-gfx-ddd-shared`, `shared/legend-auto.ts`). EDGY takes a
10
+ * dependency on that package for the generic and the box prefab alone; moving
11
+ * them to a neutral home would be the tidier answer and is not worth the churn
12
+ * of this fix.
13
+ *
14
+ * Every row is DERIVED — from the metamodel (`./metamodel.ts`: which elements
15
+ * exist, which zone each belongs to, what colour that zone is drawn in) and
16
+ * from the role vocabulary's own labels (`./roles.ts`). Nothing here is
17
+ * restated: a thirteenth element added to the metamodel gets its legend row the
18
+ * same way it gets its role, with no edit to this file.
19
+ *
20
+ * ## The order
21
+ *
22
+ * By ZONE, and the order of the table carries it: the three facets first
23
+ * (Identity, Architecture, Experience), then the three intersections in one
24
+ * section, then the four base elements, then the relations — which is the
25
+ * reading order of {@link EDGY_ZONES} and, before it, of the Venn itself.
26
+ */
27
+ /** One row per official element of `zone`, in the metamodel's own order. */
28
+ function zoneEntries(zone) {
29
+ return Object.entries(EDGY_DYNAMIC_NODES)
30
+ .filter(([, node]) => node.zone === zone)
31
+ .map(([name]) => ({
32
+ role: EDGY_ROLE[name],
33
+ row: {
34
+ swatch: 'square',
35
+ // The zone's fill, which IS what the diagram paints the element with.
36
+ color: EDGY_ZONE_FILL[zone],
37
+ label: roleLabel(EDGY_ROLES, EDGY_ROLE[name]),
38
+ },
39
+ }));
40
+ }
41
+ const FACET_SECTIONS = EDGY_ZONES.filter(zone => zone.group === 'facet').map(zone => ({
42
+ title: edgyElementLabel(zone.id),
43
+ entries: zoneEntries(zone.id),
44
+ }));
45
+ /**
46
+ * The three intersection elements in ONE section: Organisation, Product and
47
+ * Brand are not three facets, they are the three lenses between them, and the
48
+ * diagram says so by drawing them in the overlaps. Each keeps its own colour.
49
+ */
50
+ const INTERSECTIONS_SECTION = {
51
+ title: 'Intersections',
52
+ entries: EDGY_ZONES.filter(zone => zone.group === 'intersection').flatMap(zone => zoneEntries(zone.id)),
53
+ };
54
+ /**
55
+ * The four base elements, listed only when one is on the board BARE — an
56
+ * element the user dropped from the toolbox and left as a People, an Outcome,
57
+ * an Object or an Activity, without saying which of the twelve it is. White,
58
+ * because that is the fill the palette gives them (`node/consts.ts`).
59
+ *
60
+ * `exact` is what makes "bare" true. Detection is otherwise an ancestor walk,
61
+ * and the twelve official elements specialise these four — Content is an
62
+ * object, Story an activity — so an "Object" row would appear on a board
63
+ * carrying nothing but Contents, keyed to a white swatch that is drawn nowhere
64
+ * on it. The relation entry below keeps the walk, because there the parent row
65
+ * IS the fair summary: a board carrying `edgy:expresses` carries a relation.
66
+ */
67
+ const BASE_SECTION = {
68
+ title: 'Base elements',
69
+ entries: ['people', 'outcome', 'object', 'activity'].map(kind => ({
70
+ role: EDGY_ROLE[kind],
71
+ exact: true,
72
+ row: {
73
+ swatch: 'square',
74
+ color: NODE_FILL,
75
+ label: roleLabel(EDGY_ROLES, EDGY_ROLE[kind]),
76
+ },
77
+ })),
78
+ };
79
+ /**
80
+ * ONE row for the whole relation family, keyed on the PARENT role: a board
81
+ * carrying "content expresses purpose" carries `edgy:expresses`, which is a
82
+ * relation, and that is all the legend has to say. Twenty-two rows naming the
83
+ * verbs would restate the metamodel rather than document the drawing — and the
84
+ * verb travels on the link itself, visible, where it belongs.
85
+ */
86
+ const RELATIONS_SECTION = {
87
+ title: 'Relations',
88
+ entries: [
89
+ {
90
+ role: EDGY_ROLE.relation,
91
+ row: {
92
+ swatch: 'line',
93
+ // The stroke `activateEdgyRelation` arms the connector tool with.
94
+ color: NODE_STROKE,
95
+ label: roleLabel(EDGY_ROLES, EDGY_ROLE.relation),
96
+ },
97
+ },
98
+ ],
99
+ };
100
+ export const EDGY_AUTO_LEGEND = {
101
+ title: 'Legend',
102
+ roles: EDGY_ROLES,
103
+ sections: [
104
+ ...FACET_SECTIONS,
105
+ INTERSECTIONS_SECTION,
106
+ BASE_SECTION,
107
+ RELATIONS_SECTION,
108
+ ],
109
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The EDGY metamodel, as DATA — the 12 official elements and the 24 canonical
3
+ * relations that link them.
4
+ *
5
+ * It used to live in `templates/index.ts`, where it was drawn. It moved here
6
+ * the day the roles started being DERIVED from it (`./roles.ts`): the template
7
+ * needs the vocabulary to stamp its elements, and the vocabulary needs the
8
+ * metamodel to know which verbs exist, so a single module holding both would be
9
+ * a cycle. Nothing was renamed — `templates/index.ts` re-exports both tables
10
+ * under the names they always had.
11
+ *
12
+ * This file is the ONE place the metamodel is written down. `roles.ts` derives
13
+ * an edge role per verb from it, `rules.ts` derives the sanctioned sentences of
14
+ * `edgy.non-canonical-link` from it, and the template draws it. Restating the
15
+ * matrix anywhere else would be inviting the three to disagree.
16
+ */
17
+ /**
18
+ * Official pastel fills per zone (the `pictograms/Shape-*.svg` colors) — the
19
+ * colour an element of that zone is DRAWN with, on the diagram and therefore in
20
+ * the legend that documents it.
21
+ *
22
+ * Exported because the legend needs the same swatch the board uses: a swatch
23
+ * restated as a second literal would be a colour free to drift away from the
24
+ * one the eye sees.
25
+ */
26
+ export declare const EDGY_ZONE_FILL: {
27
+ readonly identity: "#80ffb7";
28
+ readonly architecture: "#a6c0ff";
29
+ readonly experience: "#ff99bd";
30
+ readonly organisation: "#80eaff";
31
+ readonly brand: "#ffd580";
32
+ readonly product: "#e599ff";
33
+ };
34
+ /** One of the six regions of the Venn: three facets, three intersections. */
35
+ export type EdgyZone = keyof typeof EDGY_ZONE_FILL;
36
+ /**
37
+ * The six zones, in reading order — the three FACETS first, then the three
38
+ * INTERSECTIONS, each named by the pair of circles it is the lens of (the very
39
+ * pairs `element-renderer.ts` clips: A∩B, B∩C, A∩C).
40
+ *
41
+ * Written down as data because the split is otherwise readable only inside the
42
+ * renderer's paint calls, and the legend has to group the twelve elements by
43
+ * it. `group` is the only thing here a reader could not already derive from
44
+ * {@link EDGY_DYNAMIC_NODES}.
45
+ */
46
+ export declare const EDGY_ZONES: readonly {
47
+ id: EdgyZone;
48
+ group: 'facet' | 'intersection';
49
+ }[];
50
+ /**
51
+ * The 24 canonical EDGY relations (source, target, verb, label position along
52
+ * the link) — exported for the unit tests. 7 per facet + 3 between the
53
+ * intersections. The optional 4th member mirrors the reference diagram's
54
+ * placements: verbs of intersection-outgoing links sit near the far element
55
+ * (`labelOffset.distance` ≈ .75), short peer links keep the middle.
56
+ *
57
+ * Read the way `docs/adr/0010` reads every typed edge: the SOURCE is the
58
+ * subject of the verb and the TARGET its object, so each row is one sentence a
59
+ * practitioner would say out loud — "content expresses purpose". That is what
60
+ * makes the row usable as an {@link EndpointTriplet} without a single
61
+ * coordinate taking part.
62
+ */
63
+ export declare const EDGY_DYNAMIC_RELATIONS: [string, string, string, number?][];
64
+ /**
65
+ * The 12 elements, centred coordinates in REFERENCE coords (the fixed space
66
+ * of consts.ts — the same space as `VENN`), laid out like the reference
67
+ * "elements & relations" diagram: aligned top row, Story/Capability flanks,
68
+ * Brand/Product astride the white centre, Task/Journey/Channel triangle.
69
+ * Exported (with `dynToModel`) for the containment test.
70
+ *
71
+ * `kind` is the OFFICIAL kind of each element — the base shape EDGY draws it
72
+ * with — and `roles.ts` reads it to give each of the twelve leaf roles its
73
+ * parent. Written once, here, rather than restated as a second table nobody
74
+ * would think to keep in step.
75
+ *
76
+ * `zone` is the region of the Venn the element belongs to, and it REPLACES the
77
+ * fill this table used to carry: the fill is `EDGY_ZONE_FILL[zone]` and nothing
78
+ * else, so an element cannot be filed under one facet and painted another's
79
+ * colour. The template reads it to draw, the legend reads it to group and to
80
+ * swatch.
81
+ */
82
+ export type EdgyElementName = 'content' | 'purpose' | 'organisation' | 'process' | 'asset' | 'story' | 'capability' | 'brand' | 'product' | 'task' | 'journey' | 'channel';
83
+ /**
84
+ * The metamodel's own wording for one of its lowercase names — one of the
85
+ * twelve elements, or one of the six zones. It is the name written on the
86
+ * diagram, so it is also what the vocabulary falls back to and what the legend
87
+ * prints as a row label or a section title. One expression, one wording.
88
+ */
89
+ export declare const edgyElementLabel: (name: EdgyElementName | EdgyZone) => string;
90
+ export declare const EDGY_DYNAMIC_NODES: Record<EdgyElementName, {
91
+ kind: 'outcome' | 'object' | 'activity';
92
+ cx: number;
93
+ cy: number;
94
+ w?: number;
95
+ zone: EdgyZone;
96
+ }>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The EDGY metamodel, as DATA — the 12 official elements and the 24 canonical
3
+ * relations that link them.
4
+ *
5
+ * It used to live in `templates/index.ts`, where it was drawn. It moved here
6
+ * the day the roles started being DERIVED from it (`./roles.ts`): the template
7
+ * needs the vocabulary to stamp its elements, and the vocabulary needs the
8
+ * metamodel to know which verbs exist, so a single module holding both would be
9
+ * a cycle. Nothing was renamed — `templates/index.ts` re-exports both tables
10
+ * under the names they always had.
11
+ *
12
+ * This file is the ONE place the metamodel is written down. `roles.ts` derives
13
+ * an edge role per verb from it, `rules.ts` derives the sanctioned sentences of
14
+ * `edgy.non-canonical-link` from it, and the template draws it. Restating the
15
+ * matrix anywhere else would be inviting the three to disagree.
16
+ */
17
+ /**
18
+ * Official pastel fills per zone (the `pictograms/Shape-*.svg` colors) — the
19
+ * colour an element of that zone is DRAWN with, on the diagram and therefore in
20
+ * the legend that documents it.
21
+ *
22
+ * Exported because the legend needs the same swatch the board uses: a swatch
23
+ * restated as a second literal would be a colour free to drift away from the
24
+ * one the eye sees.
25
+ */
26
+ export const EDGY_ZONE_FILL = {
27
+ identity: '#80ffb7',
28
+ architecture: '#a6c0ff',
29
+ experience: '#ff99bd',
30
+ organisation: '#80eaff',
31
+ brand: '#ffd580',
32
+ product: '#e599ff',
33
+ };
34
+ /**
35
+ * The six zones, in reading order — the three FACETS first, then the three
36
+ * INTERSECTIONS, each named by the pair of circles it is the lens of (the very
37
+ * pairs `element-renderer.ts` clips: A∩B, B∩C, A∩C).
38
+ *
39
+ * Written down as data because the split is otherwise readable only inside the
40
+ * renderer's paint calls, and the legend has to group the twelve elements by
41
+ * it. `group` is the only thing here a reader could not already derive from
42
+ * {@link EDGY_DYNAMIC_NODES}.
43
+ */
44
+ export const EDGY_ZONES = [
45
+ { id: 'identity', group: 'facet' },
46
+ { id: 'architecture', group: 'facet' },
47
+ { id: 'experience', group: 'facet' },
48
+ // Identity ∩ Architecture, Architecture ∩ Experience, Experience ∩ Identity.
49
+ { id: 'organisation', group: 'intersection' },
50
+ { id: 'product', group: 'intersection' },
51
+ { id: 'brand', group: 'intersection' },
52
+ ];
53
+ /**
54
+ * The 24 canonical EDGY relations (source, target, verb, label position along
55
+ * the link) — exported for the unit tests. 7 per facet + 3 between the
56
+ * intersections. The optional 4th member mirrors the reference diagram's
57
+ * placements: verbs of intersection-outgoing links sit near the far element
58
+ * (`labelOffset.distance` ≈ .75), short peer links keep the middle.
59
+ *
60
+ * Read the way `docs/adr/0010` reads every typed edge: the SOURCE is the
61
+ * subject of the verb and the TARGET its object, so each row is one sentence a
62
+ * practitioner would say out loud — "content expresses purpose". That is what
63
+ * makes the row usable as an {@link EndpointTriplet} without a single
64
+ * coordinate taking part.
65
+ */
66
+ export const EDGY_DYNAMIC_RELATIONS = [
67
+ // Identity
68
+ ['content', 'purpose', 'expresses'],
69
+ ['content', 'story', 'conveys', 0.75],
70
+ ['story', 'purpose', 'contextualises', 0.8],
71
+ ['organisation', 'purpose', 'pursues', 0.8],
72
+ ['organisation', 'story', 'authors', 0.6],
73
+ ['brand', 'purpose', 'represents', 0.8],
74
+ ['brand', 'story', 'evokes', 0.65],
75
+ // Architecture
76
+ ['organisation', 'process', 'performs', 0.8],
77
+ ['process', 'capability', 'realises', 0.75],
78
+ ['process', 'asset', 'requires'],
79
+ ['capability', 'asset', 'requires', 0.75],
80
+ ['organisation', 'capability', 'has', 0.6],
81
+ ['product', 'capability', 'requires', 0.75],
82
+ ['process', 'product', 'creates', 0.65],
83
+ // Experience
84
+ ['task', 'journey', 'is part of', 0.6],
85
+ ['task', 'channel', 'uses', 0.6],
86
+ ['journey', 'channel', 'traverses', 0.6],
87
+ ['product', 'task', 'serves', 0.9],
88
+ ['product', 'journey', 'features in', 0.8],
89
+ ['brand', 'task', 'supports', 0.8],
90
+ ['brand', 'journey', 'appears in', 0.9],
91
+ // Intersections
92
+ ['organisation', 'brand', 'builds', 0.85],
93
+ ['organisation', 'product', 'makes', 0.65],
94
+ ['product', 'brand', 'embodies', 0.8],
95
+ ];
96
+ /**
97
+ * The metamodel's own wording for one of its lowercase names — one of the
98
+ * twelve elements, or one of the six zones. It is the name written on the
99
+ * diagram, so it is also what the vocabulary falls back to and what the legend
100
+ * prints as a row label or a section title. One expression, one wording.
101
+ */
102
+ export const edgyElementLabel = (name) => name.charAt(0).toUpperCase() + name.slice(1);
103
+ export const EDGY_DYNAMIC_NODES = {
104
+ content: { kind: 'object', cx: 237.5, cy: 100, zone: 'identity' },
105
+ purpose: { kind: 'outcome', cx: 282.5, cy: 100, zone: 'identity' },
106
+ organisation: {
107
+ kind: 'object',
108
+ cx: 340,
109
+ cy: 100,
110
+ w: 175,
111
+ zone: 'organisation',
112
+ },
113
+ process: { kind: 'activity', cx: 397.5, cy: 100, zone: 'architecture' },
114
+ asset: { kind: 'object', cx: 442.5, cy: 100, zone: 'architecture' },
115
+ story: { kind: 'activity', cx: 255, cy: 152.5, zone: 'identity' },
116
+ capability: {
117
+ kind: 'outcome',
118
+ cx: 425,
119
+ cy: 152.5,
120
+ w: 150,
121
+ zone: 'architecture',
122
+ },
123
+ brand: { kind: 'object', cx: 280, cy: 195, zone: 'brand' },
124
+ product: { kind: 'object', cx: 400, cy: 195, zone: 'product' },
125
+ task: { kind: 'outcome', cx: 310, cy: 257.5, zone: 'experience' },
126
+ journey: { kind: 'activity', cx: 370, cy: 257.5, zone: 'experience' },
127
+ channel: { kind: 'object', cx: 340, cy: 297.5, zone: 'experience' },
128
+ };
@@ -0,0 +1,26 @@
1
+ import type { QualityNudge } from '@formicoidea/labre-core/blocks/surface';
2
+ /**
3
+ * EDGY **work quality** — the checklist (WS1).
4
+ *
5
+ * Level 1 is `./rules.ts`: deterministic, decidable, real time. This file is
6
+ * level 2, and the split is the whole point of the taxonomy — a rule only
7
+ * enters the deterministic engine if an algorithm can decide it on persisted
8
+ * data inside the ~16 ms budget. Everything else is a NUDGE: an expectation
9
+ * offered as a checklist, never evaluated, never blocking anything. Ticking is
10
+ * assuming.
11
+ *
12
+ * ## Why q1 and q2 are here and not in `rules.ts` (PO arbitration, 26/08/2026)
13
+ *
14
+ * They read like rules, and they are not. "Each intersection element is linked
15
+ * to both of its parent facets" would need the tool to know which circle an
16
+ * element belongs to — and nothing says so except where somebody dropped it,
17
+ * which is the layout answering a question about meaning. "Each element wears
18
+ * its facet's colour" would indict every board whose author uses their own
19
+ * palette, which is most of them after the first workshop. Both are judgements
20
+ * a modeller makes; the tool can put them on screen and cannot answer them.
21
+ *
22
+ * Registered from the flag-gated `EdgyViewExtension`, beside the rules and the
23
+ * profiles: a checklist is tooling. Switching the flag off takes it away and
24
+ * leaves the ticks written on the board, unread, until it comes back (PF7.10).
25
+ */
26
+ export declare const EDGY_NUDGES: readonly QualityNudge[];
package/dist/nudges.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * EDGY **work quality** — the checklist (WS1).
3
+ *
4
+ * Level 1 is `./rules.ts`: deterministic, decidable, real time. This file is
5
+ * level 2, and the split is the whole point of the taxonomy — a rule only
6
+ * enters the deterministic engine if an algorithm can decide it on persisted
7
+ * data inside the ~16 ms budget. Everything else is a NUDGE: an expectation
8
+ * offered as a checklist, never evaluated, never blocking anything. Ticking is
9
+ * assuming.
10
+ *
11
+ * ## Why q1 and q2 are here and not in `rules.ts` (PO arbitration, 26/08/2026)
12
+ *
13
+ * They read like rules, and they are not. "Each intersection element is linked
14
+ * to both of its parent facets" would need the tool to know which circle an
15
+ * element belongs to — and nothing says so except where somebody dropped it,
16
+ * which is the layout answering a question about meaning. "Each element wears
17
+ * its facet's colour" would indict every board whose author uses their own
18
+ * palette, which is most of them after the first workshop. Both are judgements
19
+ * a modeller makes; the tool can put them on screen and cannot answer them.
20
+ *
21
+ * Registered from the flag-gated `EdgyViewExtension`, beside the rules and the
22
+ * profiles: a checklist is tooling. Switching the flag off takes it away and
23
+ * leaves the ticks written on the board, unread, until it comes back (PF7.10).
24
+ */
25
+ export const EDGY_NUDGES = [
26
+ {
27
+ id: 'edgy.q1-intersection-links',
28
+ framework: 'edgy',
29
+ labelKey: 'com.labre.edgy.quality.intersection-links',
30
+ fallback: 'Each intersection element is linked to both of its parent facets.',
31
+ order: 1,
32
+ },
33
+ {
34
+ id: 'edgy.q2-facet-colour',
35
+ framework: 'edgy',
36
+ labelKey: 'com.labre.edgy.quality.facet-colour',
37
+ fallback: "Each element wears its facet's colour.",
38
+ order: 2,
39
+ },
40
+ {
41
+ id: 'edgy.q3-readable-relations',
42
+ framework: 'edgy',
43
+ labelKey: 'com.labre.edgy.quality.readable-relations',
44
+ fallback: 'Relations read correctly (the source is the subject of the verb).',
45
+ order: 3,
46
+ },
47
+ {
48
+ id: 'edgy.q4-three-facets',
49
+ framework: 'edgy',
50
+ labelKey: 'com.labre.edgy.quality.three-facets',
51
+ fallback: 'All three facets have been explored.',
52
+ order: 4,
53
+ },
54
+ ];
@@ -0,0 +1,2 @@
1
+ import type { ValidationProfile } from '@formicoidea/labre-core/blocks/surface';
2
+ export declare const EDGY_PROFILES: readonly ValidationProfile[];