@formicoidea/labre-framework-edgy 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 (48) hide show
  1. package/dist/actions.d.ts +39 -0
  2. package/dist/actions.js +193 -0
  3. package/dist/board-renderer.d.ts +12 -0
  4. package/dist/board-renderer.js +28 -0
  5. package/dist/board-view.d.ts +11 -0
  6. package/dist/board-view.js +20 -0
  7. package/dist/commands.d.ts +4 -0
  8. package/dist/commands.js +103 -0
  9. package/dist/consts.d.ts +35 -0
  10. package/dist/consts.js +46 -0
  11. package/dist/descriptor.d.ts +8 -3
  12. package/dist/descriptor.js +6 -3
  13. package/dist/element-renderer.js +24 -13
  14. package/dist/element-view.js +9 -3
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.js +2 -1
  17. package/dist/label-layout.js +8 -2
  18. package/dist/legend.d.ts +2 -0
  19. package/dist/legend.js +109 -0
  20. package/dist/metamodel.d.ts +96 -0
  21. package/dist/metamodel.js +128 -0
  22. package/dist/nudges.d.ts +26 -0
  23. package/dist/nudges.js +54 -0
  24. package/dist/profiles.d.ts +2 -0
  25. package/dist/profiles.js +66 -0
  26. package/dist/relation-resolver.d.ts +32 -0
  27. package/dist/relation-resolver.js +101 -0
  28. package/dist/relation.d.ts +142 -0
  29. package/dist/relation.js +188 -0
  30. package/dist/roles.d.ts +108 -0
  31. package/dist/roles.js +182 -0
  32. package/dist/rules.d.ts +43 -0
  33. package/dist/rules.js +153 -0
  34. package/dist/templates/index.d.ts +22 -1
  35. package/dist/templates/index.js +303 -52
  36. package/dist/toolbar/config.d.ts +33 -0
  37. package/dist/toolbar/config.js +82 -15
  38. package/dist/toolbar/edgy-menu.d.ts +7 -25
  39. package/dist/toolbar/edgy-menu.js +7 -186
  40. package/dist/toolbar/edgy-senior-button.js +8 -2
  41. package/dist/toolbar/icons.d.ts +12 -0
  42. package/dist/toolbar/icons.js +31 -0
  43. package/dist/toolbar/senior-tool.js +1 -0
  44. package/dist/translations.d.ts +24 -0
  45. package/dist/translations.js +29 -0
  46. package/dist/view.d.ts +18 -0
  47. package/dist/view.js +95 -8
  48. package/package.json +6 -3
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[];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * EDGY validation profiles (WS1).
3
+ *
4
+ * DATA owned by the framework, like its rules and its roles: the engine knows
5
+ * how to APPLY a profile, never which ones EDGY has. Adding a level of
6
+ * requirement is adding an entry to this array.
7
+ *
8
+ * A profile is chosen per BACKGROUND, not per document (PF9.1) — a workshop
9
+ * scribble and a deliverable model coexist on the same canvas with different
10
+ * requirements, and the choice rides on the facets diagram or board itself.
11
+ *
12
+ * Registered from the flag-gated `EdgyViewExtension`, beside the rules:
13
+ * switching the EDGY flag off takes the choice away with the rest of the
14
+ * tooling, and a board already set to `strict` simply stops being checked until
15
+ * it comes back — the id stays written, untouched.
16
+ */
17
+ /**
18
+ * Sketch: every rule demoted to `audit`, so findings still reach `violations$`
19
+ * — a host panel and a conformance report see them — and the canvas says
20
+ * nothing at all.
21
+ *
22
+ * The DEFAULT, and deliberately so (PRD principle 3). EDGY is a WORKSHOP
23
+ * notation: elements land on the diagram in the order people say them, half the
24
+ * relations point the wrong way for the ten minutes it takes to argue about
25
+ * them, and boxes pile up in a corner before anybody sorts them into facets. A
26
+ * tool warning about all of that in real time would be arguing with the
27
+ * workshop.
28
+ *
29
+ * Being the default also means it is the one profile that WRITES NOTHING: a
30
+ * board on `sketch` carries no profile key, so every EDGY diagram ever drawn is
31
+ * on it, with no migration and no backfill.
32
+ */
33
+ const sketch = {
34
+ id: 'edgy.sketch',
35
+ framework: 'edgy',
36
+ labelKey: 'com.labre.edgy.profile.sketch',
37
+ fallback: 'Sketch',
38
+ isDefault: true,
39
+ rules: {
40
+ 'edgy.non-canonical-link': 'audit',
41
+ 'edgy.overlapping-artefacts': 'audit',
42
+ },
43
+ };
44
+ /**
45
+ * Strict: the deliverable level. Both rules bite, and both severities are
46
+ * spelled out here rather than left absent — PF9.4 wants the answer readable in
47
+ * one place, and a profile that says "warning" out loud cannot be misread as
48
+ * "whatever the rule happens to declare this quarter".
49
+ *
50
+ * `warning` and not `blocking-overridable` for E1, though a relation outside the
51
+ * metamodel is exactly the statement a strict EDGY model must not contain:
52
+ * nothing in this library implements a blocking level, so the value would claim
53
+ * an effect that does not exist. When the gesture refusal lands, this is a
54
+ * two-word change and this file is the only one that moves.
55
+ */
56
+ const strict = {
57
+ id: 'edgy.strict',
58
+ framework: 'edgy',
59
+ labelKey: 'com.labre.edgy.profile.strict',
60
+ fallback: 'Strict',
61
+ rules: {
62
+ 'edgy.non-canonical-link': 'warning',
63
+ 'edgy.overlapping-artefacts': 'warning',
64
+ },
65
+ };
66
+ export const EDGY_PROFILES = [sketch, strict];
@@ -0,0 +1,32 @@
1
+ import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
2
+ /**
3
+ * Turns "a connector was drawn, or one of its ends just moved" into a naming.
4
+ *
5
+ * Registered from the FLAG-GATED `EdgyViewExtension`, not from the always-on
6
+ * render one, and the distinction is `docs/adr/0009`'s: this is a creation
7
+ * tool. It authors content — it writes a role and a label into the document —
8
+ * and it only ever fires on an edge the flag-gated toolbox itself stamped
9
+ * `edgy:relation`. A board drawn while the flag was on keeps every verb it was
10
+ * given, painted by the always-on renderer and read by the always-on
11
+ * vocabulary; with the flag off nothing new is named because nothing new is
12
+ * being armed. Registering it always-on would mean the library kept authoring
13
+ * EDGY content for a host that switched EDGY off.
14
+ *
15
+ * The subscription mechanics are `EstuarineGhostManager`'s, including the part
16
+ * that matters: the surface is a SIGNAL, null at mount and replaced when the
17
+ * surface block is.
18
+ */
19
+ export declare class EdgyRelationResolver extends InteractivityExtension {
20
+ static key: string;
21
+ private _subscriptions;
22
+ private _disposeSurfaceEffect;
23
+ private _pending;
24
+ private _candidates;
25
+ mounted(): void;
26
+ unmounted(): void;
27
+ private _forget;
28
+ private _unsubscribe;
29
+ private _resubscribe;
30
+ private _schedule;
31
+ private _flush;
32
+ }
@@ -0,0 +1,101 @@
1
+ import { InteractivityExtension } from '@formicoidea/labre-core/std/gfx';
2
+ import { effect } from '@preact/signals-core';
3
+ import { EDGY_RELATION_DELAY_MS, resolveEdgyRelations } from './relation.js';
4
+ /**
5
+ * Turns "a connector was drawn, or one of its ends just moved" into a naming.
6
+ *
7
+ * Registered from the FLAG-GATED `EdgyViewExtension`, not from the always-on
8
+ * render one, and the distinction is `docs/adr/0009`'s: this is a creation
9
+ * tool. It authors content — it writes a role and a label into the document —
10
+ * and it only ever fires on an edge the flag-gated toolbox itself stamped
11
+ * `edgy:relation`. A board drawn while the flag was on keeps every verb it was
12
+ * given, painted by the always-on renderer and read by the always-on
13
+ * vocabulary; with the flag off nothing new is named because nothing new is
14
+ * being armed. Registering it always-on would mean the library kept authoring
15
+ * EDGY content for a host that switched EDGY off.
16
+ *
17
+ * The subscription mechanics are `EstuarineGhostManager`'s, including the part
18
+ * that matters: the surface is a SIGNAL, null at mount and replaced when the
19
+ * surface block is.
20
+ */
21
+ export class EdgyRelationResolver extends InteractivityExtension {
22
+ constructor() {
23
+ super(...arguments);
24
+ this._subscriptions = [];
25
+ this._disposeSurfaceEffect = null;
26
+ this._pending = null;
27
+ this._candidates = new Set();
28
+ }
29
+ static { this.key = 'edgy-relation-resolver'; }
30
+ mounted() {
31
+ this._disposeSurfaceEffect = effect(() => {
32
+ this._resubscribe(this.gfx.surface$.value);
33
+ });
34
+ }
35
+ unmounted() {
36
+ this._disposeSurfaceEffect?.();
37
+ this._disposeSurfaceEffect = null;
38
+ this._unsubscribe();
39
+ this._forget();
40
+ super.unmounted();
41
+ }
42
+ _forget() {
43
+ if (this._pending)
44
+ clearTimeout(this._pending);
45
+ this._pending = null;
46
+ this._candidates.clear();
47
+ }
48
+ _unsubscribe() {
49
+ for (const subscription of this._subscriptions)
50
+ subscription.unsubscribe();
51
+ this._subscriptions = [];
52
+ }
53
+ _resubscribe(surface) {
54
+ this._unsubscribe();
55
+ // Candidates queued against the previous surface are about elements this
56
+ // one has never heard of.
57
+ this._forget();
58
+ if (!surface)
59
+ return;
60
+ this._subscriptions.push(surface.elementAdded.subscribe(({ id, local }) => {
61
+ if (!local)
62
+ return;
63
+ this._schedule(id);
64
+ }));
65
+ this._subscriptions.push(surface.elementUpdated.subscribe(({ id, props, local }) => {
66
+ // LOCAL only, the filter `reading.ts` documents at length: `local`
67
+ // partitions the fleet into one writer and N−1 silent observers with
68
+ // no leader election. Without it, everyone on a shared board would
69
+ // name the same relation the moment it synced — the same role and the
70
+ // same word each time, so nothing would corrupt, but each peer would
71
+ // get an undo entry for a gesture they did not make.
72
+ if (!local)
73
+ return;
74
+ // Only the two ends can turn an unnamed relation into a sentence.
75
+ // Every other prop — a drag, a restyle, a label edit — must not even
76
+ // rearm the timer.
77
+ if (!props || (!('source' in props) && !('target' in props)))
78
+ return;
79
+ this._schedule(id);
80
+ }));
81
+ }
82
+ _schedule(id) {
83
+ this._candidates.add(id);
84
+ if (this._pending)
85
+ clearTimeout(this._pending);
86
+ this._pending = setTimeout(() => {
87
+ this._pending = null;
88
+ this._flush();
89
+ }, EDGY_RELATION_DELAY_MS);
90
+ }
91
+ _flush() {
92
+ const surface = this.gfx.surface;
93
+ const candidates = this._candidates;
94
+ // Whatever happens below, what accumulated is accounted for: a set left
95
+ // behind would be replayed against a later, unrelated gesture.
96
+ this._candidates = new Set();
97
+ if (!surface)
98
+ return;
99
+ resolveEdgyRelations(surface, candidates);
100
+ }
101
+ }