yarramate 0.20.0 → 0.22.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 (34) hide show
  1. package/dist/adapters/visual/protocol-contract.d.ts +13 -1
  2. package/dist/adapters/visual/protocol-contract.js +1 -1
  3. package/dist/adapters/visual/protocol.d.ts +11 -0
  4. package/dist/adapters/visual/protocol.js +12 -0
  5. package/dist/adapters/visual/request.js +1 -3
  6. package/dist/adapters/visual/session-server.js +55 -1
  7. package/dist/adapters/visual/wire.d.ts +7 -0
  8. package/dist/adapters/visual-graph-entry.d.ts +1 -0
  9. package/dist/adapters/visual-graph-entry.js +1 -0
  10. package/dist/apply-command.js +132 -13
  11. package/dist/compiler.d.ts +1 -19
  12. package/dist/compiler.js +2 -35
  13. package/dist/graph-claims.d.ts +18 -0
  14. package/dist/graph-claims.js +35 -0
  15. package/dist/graph-projection.d.ts +4 -3
  16. package/dist/graph-projection.js +1 -1
  17. package/dist/notation/archimate.d.ts +71 -0
  18. package/dist/notation/archimate.js +183 -0
  19. package/dist/operations.d.ts +18 -0
  20. package/dist/subject-references.d.ts +92 -0
  21. package/dist/subject-references.js +226 -0
  22. package/dist/visual-app/assets/index-CRd1Khma.js +354 -0
  23. package/dist/visual-app/assets/{index-dwA9AOr5.css → index-tZSHlU-a.css} +1 -1
  24. package/dist/visual-app/index.html +2 -2
  25. package/docs/CONSUMING-YARRAMATE.md +26 -1
  26. package/package.json +9 -1
  27. package/schema/yarramate-apply-result.schema.json +4 -0
  28. package/schema/yarramate-operations.schema.json +54 -3
  29. package/schema/yarramate-visual-event.schema.json +11 -1
  30. package/schema/yarramate-visual-graph.schema.json +27 -2
  31. package/schema/yarramate-visual-session-descriptor.schema.json +1 -1
  32. package/schema/yarramate-visual-session-started.schema.json +1 -1
  33. package/schema/yarramate-visual-status.schema.json +1 -1
  34. package/dist/visual-app/assets/index-6oSR9RGM.js +0 -354
@@ -0,0 +1,183 @@
1
+ // Single source of truth for ArchiMate-inspired rendering data: layer
2
+ // palette, aspect-driven shape tokens, per-concept-kind glyphs, and
3
+ // per-relationship-kind line/arrow styles. Pure data + lookups, no
4
+ // cytoscape/DOM dependency - the same zero-import discipline as
5
+ // `src/visual-app/kind-icons.ts` and `badges.ts`, so this module can be
6
+ // published standalone (`yarramate/notation/archimate`) and consumed by
7
+ // non-canvas renderers. Not wired into `graph-canvas.tsx` yet (Task 4
8
+ // switches the canvas to read from here instead of its own inline tables).
9
+ import { conceptKinds, layers, relationshipKinds } from '../profile.js';
10
+ export const ICON_SIZE = 14;
11
+ const INK = '#182228'; // --ink (src/visual-app/styles.css:12)
12
+ // The locked canvas layer palette (`graph-canvas.tsx`'s `LAYER_COLORS`
13
+ // selectors) - `physical` and `composite` deliberately share the same
14
+ // neutral grey since neither carries a distinct layer identity of its own.
15
+ export const LAYER_COLORS = {
16
+ motivation: { fill: '#CCCCFF', border: '#8F8FE0' },
17
+ strategy: { fill: '#F5DEAA', border: '#C9A355' },
18
+ business: { fill: '#FFFF99', border: '#C9C355' },
19
+ application: { fill: '#CCFFFF', border: '#4FB8B8' },
20
+ technology: { fill: '#CCFFCC', border: '#5FAE5F' },
21
+ implementation: { fill: '#FFE0E0', border: '#D89999' },
22
+ physical: { fill: '#F0F0F0', border: '#999999' },
23
+ composite: { fill: '#F0F0F0', border: '#999999' },
24
+ };
25
+ // Aspect -> shape/accent/borderStyle, mirroring `graph-canvas.tsx`'s
26
+ // `node[aspect = "…"]` selectors (Task 8/11 ArchiMate notation mode).
27
+ export const ASPECT_SHAPES = {
28
+ motivation: { shape: 'octagon' },
29
+ 'active-structure': { shape: 'rectangle' },
30
+ behavior: { shape: 'round-rectangle' },
31
+ 'passive-structure': { shape: 'rectangle', accent: 'top-band' },
32
+ composite: { shape: 'rectangle', borderStyle: 'dashed' },
33
+ };
34
+ function svg(body) {
35
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${ICON_SIZE}" height="${ICON_SIZE}" ` +
36
+ `viewBox="0 0 ${ICON_SIZE} ${ICON_SIZE}">` +
37
+ `<g fill="none" stroke="${INK}" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">${body}</g>` +
38
+ `</svg>`);
39
+ }
40
+ function toDataUri(svgMarkup) {
41
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svgMarkup)}`;
42
+ }
43
+ // The 17 kinds the canvas already draws a glyph for (`src/visual-app/
44
+ // kind-icons.ts` `BASE_KIND_SVG`), copied verbatim. Every other core kind
45
+ // (e.g. `stakeholder`) has no glyph body and resolves to `null` below -
46
+ // that's an allowed gap, not a coverage failure: `CONCEPT_NOTATION` still
47
+ // carries a row for it, just with `glyph: null`.
48
+ const BASE_KIND_SVG = {
49
+ // Component: UML/ArchiMate component box with two connector notches on
50
+ // its left edge.
51
+ applicationComponent: '<rect x="3" y="2" width="9" height="10"/>' +
52
+ '<rect x="1" y="4" width="2.5" height="1.5"/>' +
53
+ '<rect x="1" y="8" width="2.5" height="1.5"/>',
54
+ // Function: rounded process box with a right-pointing chevron.
55
+ applicationFunction: '<rect x="2" y="3" width="10" height="8" rx="2"/><path d="M5.5 5 L8 7 L5.5 9"/>',
56
+ // Service: a pill/oval, ArchiMate's rounded-rectangle service shape.
57
+ applicationService: '<rect x="1.5" y="4" width="11" height="6" rx="3"/>',
58
+ // Artifact: a document with a folded top-right corner.
59
+ artifact: '<path d="M3 1.5 H9 L11 3.5 V12.5 H3 Z"/><path d="M9 1.5 V3.5 H11"/>',
60
+ // Actor: a stick figure - head, torso, arms, legs.
61
+ businessActor: '<circle cx="7" cy="3.5" r="1.8"/><path d="M7 5.3 V9 M4 7 H10 M7 9 L4.5 12.5 M7 9 L9.5 12.5"/>',
62
+ // Function: same process-box convention as applicationFunction, an
63
+ // upward chevron distinguishes the business layer.
64
+ businessFunction: '<rect x="2" y="3" width="10" height="8" rx="2"/><path d="M5 8 L7 5.5 L9 8"/>',
65
+ // Capability: a box with a compass-style arrow toward its top-right
66
+ // corner (capability-as-direction motif).
67
+ capability: '<rect x="2" y="2" width="10" height="10" rx="1"/><path d="M5 9 L9 5 M9 5 H6.5 M9 5 V7.5"/>',
68
+ // Data object: a record box with a header divider.
69
+ dataObject: '<rect x="2" y="2" width="10" height="10"/><path d="M2 5.5 H12"/>',
70
+ // Deliverable: a folded-corner document (like artifact) with a ribbon
71
+ // check-mark distinguishing it as a completed output.
72
+ deliverable: '<path d="M3 1.5 H8 L11 4.5 V12.5 H3 Z"/><path d="M8 1.5 V4.5 H11"/><path d="M5 9.5 L7 12.5 L9 9.5"/>',
73
+ // Driver: a steering-wheel motif - a circle with four radiating spokes.
74
+ driver: '<circle cx="7" cy="7" r="5"/><path d="M7 2 V5 M7 9 V12 M2 7 H5 M9 7 H12"/>',
75
+ // Goal: a target - two concentric circles.
76
+ goal: '<circle cx="7" cy="7" r="5"/><circle cx="7" cy="7" r="2.2"/>',
77
+ // Node: a 3D cuboid, ArchiMate's technology-node glyph.
78
+ node: '<path d="M2 5 L7 2.5 L12 5 L12 10 L7 12.5 L2 10 Z"/><path d="M2 5 L7 7.5 L12 5 M7 7.5 V12.5"/>',
79
+ // Plateau: a flat-topped trapezoid.
80
+ plateau: '<path d="M2 11 L4.5 4 H9.5 L12 11 Z"/><path d="M2 11 H12"/>',
81
+ // Representation: a document with a wavy bottom edge (curled paper).
82
+ representation: '<path d="M3 2 H11 V9 Q9.5 11 8 9.5 Q6.5 8 5 9.5 Q3.5 11 3 9 Z"/>',
83
+ // Requirement: a pointed banner/shield shape with a header divider.
84
+ requirement: '<path d="M3 2.5 H11 V9 L7 11.5 L3 9 Z"/><path d="M3 5 H11"/>',
85
+ // System software: a nested box, a smaller box docked inside the larger.
86
+ systemSoftware: '<rect x="2" y="2" width="10" height="10"/><rect x="4" y="4" width="4" height="3"/>',
87
+ // Function: same process convention again, technology layer gets a gear
88
+ // motif - a circle with radiating teeth.
89
+ technologyFunction: '<circle cx="7" cy="7" r="4"/>' +
90
+ '<path d="M7 2 V3.2 M7 10.8 V12 M2 7 H3.2 M10.8 7 H12 ' +
91
+ 'M3.5 3.5 L4.3 4.3 M9.7 9.7 L10.5 10.5 M3.5 10.5 L4.3 9.7 M9.7 4.3 L10.5 3.5"/>',
92
+ };
93
+ // Built from `conceptKinds` (not enumerated by hand) so coverage cannot
94
+ // drift from the core vocabulary - a new kind added to `profile.ts` gets a
95
+ // notation row automatically, and a removed one disappears automatically.
96
+ export const CONCEPT_NOTATION = conceptKinds.map((kind) => ({
97
+ id: kind.id,
98
+ notation: kind.name,
99
+ layer: kind.layer,
100
+ aspect: kind.aspect,
101
+ ...ASPECT_SHAPES[kind.aspect],
102
+ glyph: BASE_KIND_SVG[kind.id] ?? null,
103
+ colors: LAYER_COLORS[kind.layer],
104
+ }));
105
+ const CONCEPT_NOTATION_BY_ID = Object.fromEntries(CONCEPT_NOTATION.map((row) => [row.id, row]));
106
+ export function conceptNotationOf(kindLabel) {
107
+ return CONCEPT_NOTATION_BY_ID[kindLabel] ?? null;
108
+ }
109
+ export function kindGlyphDataUriOf(kindLabel) {
110
+ const glyph = CONCEPT_NOTATION_BY_ID[kindLabel]?.glyph;
111
+ return glyph == null ? null : toDataUri(svg(glyph));
112
+ }
113
+ // The 11 rows from `graph-canvas.tsx`'s ArchiMate edge selectors (Task 11).
114
+ const RELATIONSHIP_STYLE = {
115
+ composition: {
116
+ lineStyle: 'solid',
117
+ sourceArrow: { shape: 'diamond', fill: 'filled' },
118
+ targetArrow: { shape: 'none' },
119
+ },
120
+ aggregation: {
121
+ lineStyle: 'solid',
122
+ sourceArrow: { shape: 'diamond', fill: 'hollow' },
123
+ targetArrow: { shape: 'none' },
124
+ },
125
+ assignment: {
126
+ lineStyle: 'solid',
127
+ sourceArrow: { shape: 'circle', fill: 'filled' },
128
+ targetArrow: { shape: 'triangle', fill: 'filled' },
129
+ },
130
+ realization: {
131
+ lineStyle: 'dotted',
132
+ sourceArrow: { shape: 'none' },
133
+ targetArrow: { shape: 'triangle', fill: 'hollow' },
134
+ },
135
+ specialization: {
136
+ lineStyle: 'solid',
137
+ sourceArrow: { shape: 'none' },
138
+ targetArrow: { shape: 'triangle', fill: 'hollow' },
139
+ },
140
+ serving: {
141
+ lineStyle: 'solid',
142
+ sourceArrow: { shape: 'none' },
143
+ targetArrow: { shape: 'vee' },
144
+ },
145
+ access: {
146
+ lineStyle: 'dotted',
147
+ sourceArrow: { shape: 'none' },
148
+ targetArrow: { shape: 'vee' },
149
+ },
150
+ influence: {
151
+ lineStyle: 'dashed',
152
+ sourceArrow: { shape: 'none' },
153
+ targetArrow: { shape: 'vee' },
154
+ },
155
+ triggering: {
156
+ lineStyle: 'solid',
157
+ sourceArrow: { shape: 'none' },
158
+ targetArrow: { shape: 'triangle', fill: 'filled' },
159
+ },
160
+ flow: {
161
+ lineStyle: 'dashed',
162
+ sourceArrow: { shape: 'none' },
163
+ targetArrow: { shape: 'triangle', fill: 'filled' },
164
+ },
165
+ association: {
166
+ lineStyle: 'solid',
167
+ sourceArrow: { shape: 'none' },
168
+ targetArrow: { shape: 'none' },
169
+ },
170
+ };
171
+ // Built from `relationshipKinds` so coverage cannot drift, same discipline
172
+ // as `CONCEPT_NOTATION` above.
173
+ export const RELATIONSHIP_NOTATION = relationshipKinds.map((id) => ({
174
+ id,
175
+ ...RELATIONSHIP_STYLE[id],
176
+ }));
177
+ const RELATIONSHIP_NOTATION_BY_ID = Object.fromEntries(RELATIONSHIP_NOTATION.map((row) => [row.id, row]));
178
+ export function relationshipNotationOf(kindLabel) {
179
+ return RELATIONSHIP_NOTATION_BY_ID[kindLabel] ?? null;
180
+ }
181
+ // `layers` is re-exported so consumers of this subpath don't need a second
182
+ // import from `../profile.js` just to iterate the palette.
183
+ export { layers };
@@ -103,6 +103,22 @@ export type YarramateOperation = {
103
103
  readonly relationship: {
104
104
  readonly id: string;
105
105
  };
106
+ } | {
107
+ readonly op: 'rename-concept';
108
+ readonly document: string;
109
+ readonly concept: {
110
+ readonly id: string;
111
+ };
112
+ /** The local id this subject should have had. Every declarative
113
+ * reference to the old address moves with it; prose does not. */
114
+ readonly to: string;
115
+ } | {
116
+ readonly op: 'rename-relationship';
117
+ readonly document: string;
118
+ readonly relationship: {
119
+ readonly id: string;
120
+ };
121
+ readonly to: string;
106
122
  } | {
107
123
  readonly op: 'add-observation';
108
124
  readonly document: string;
@@ -131,6 +147,8 @@ export interface YarramateApplyResult {
131
147
  readonly updatedRelationships: number;
132
148
  readonly deletedConcepts: number;
133
149
  readonly deletedRelationships: number;
150
+ readonly renamedConcepts: number;
151
+ readonly renamedRelationships: number;
134
152
  readonly addedObservations: number;
135
153
  readonly updatedObservations: number;
136
154
  readonly deletedObservations: number;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Every place a subject address can appear on disk, and the surgery that moves
3
+ * one. A rename is only honest if it is total: an address left behind is a
4
+ * dangling reference or, worse, a selector that silently stops matching. The
5
+ * enumeration below is the single statement of where those addresses live, and
6
+ * `test/subject-references.test.ts` derives the same set from the JSON Schemas
7
+ * so a new reference field cannot be added without landing here too.
8
+ */
9
+ /** The four on-disk document kinds that can name a subject. */
10
+ export type SubjectReferenceGroup = 'document' | 'projection' | 'evidence' | 'adapter-mapping';
11
+ /**
12
+ * How the address is spelled at a position.
13
+ * - `declaration`: the subject's own local id, always bare, in its own document.
14
+ * - `reference`: bare local id (meaning "this document") or fully qualified.
15
+ * - `qualified`: always `document#local`, optionally with a `~aspect` suffix
16
+ * (claim addresses are minted that way - see `compiler.ts`).
17
+ */
18
+ export type SubjectReferenceForm = 'declaration' | 'reference' | 'qualified';
19
+ export interface SubjectReferencePosition {
20
+ readonly group: SubjectReferenceGroup;
21
+ /** Key path from the document root; `*` matches every sequence index. */
22
+ readonly path: readonly string[];
23
+ readonly form: SubjectReferenceForm;
24
+ }
25
+ export declare const SUBJECT_REFERENCE_POSITIONS: readonly SubjectReferencePosition[];
26
+ /**
27
+ * Positions that carry reference *syntax* but are not subject addresses, with
28
+ * the reason. Kept as data because the schema-derived completeness test asserts
29
+ * that the two lists together account for every reference-typed position: an
30
+ * omission has to be argued for here rather than forgotten.
31
+ */
32
+ export declare const EXCLUDED_REFERENCE_POSITIONS: readonly {
33
+ readonly group: SubjectReferenceGroup;
34
+ readonly path: readonly string[];
35
+ readonly reason: string;
36
+ }[];
37
+ export interface SubjectReferenceHit {
38
+ /** JSON pointer into the document, for diagnostics. */
39
+ readonly pointer: string;
40
+ /** Qualified address with any `~aspect` suffix removed. */
41
+ readonly address: string;
42
+ readonly form: SubjectReferenceForm;
43
+ /** Source offsets of the scalar's own bytes, quotes included. */
44
+ readonly start: number;
45
+ readonly end: number;
46
+ /** The scalar exactly as written, quotes included. */
47
+ readonly raw: string;
48
+ }
49
+ export interface SubjectReferenceScan {
50
+ /** The document's own id, `''` when it declares none. */
51
+ readonly documentId: string;
52
+ readonly hits: readonly SubjectReferenceHit[];
53
+ /**
54
+ * Pointers at reference positions that hold an alias node. The walker cannot
55
+ * re-point one, so a rename refuses rather than silently leaving it behind.
56
+ */
57
+ readonly aliases: readonly string[];
58
+ }
59
+ /**
60
+ * Every subject address a file of this group holds. Pure read: the parse is
61
+ * thrown away, so callers stay free to splice the original bytes.
62
+ */
63
+ export declare const scanSubjectReferences: (source: string, group: SubjectReferenceGroup) => SubjectReferenceScan;
64
+ export interface SubjectRename {
65
+ /** Qualified address as declared today. */
66
+ readonly from: string;
67
+ /** Qualified address it should have had; the document part never moves. */
68
+ readonly to: string;
69
+ }
70
+ export type SubjectRewriteResult = {
71
+ readonly ok: true;
72
+ readonly source: string;
73
+ /** Pointers this rewrite moved, in document order. */
74
+ readonly moved: readonly string[];
75
+ } | {
76
+ readonly ok: false;
77
+ readonly aliases: readonly string[];
78
+ };
79
+ /**
80
+ * Re-points every reference to `rename.from` in one file. Only the matched
81
+ * scalars' own bytes change - nothing is re-rendered, so byte identity holds
82
+ * everywhere else, a bare reference stays bare, a qualified one stays
83
+ * qualified, a `~aspect` suffix survives, and the original quoting is kept.
84
+ */
85
+ export declare const rewriteSubjectReferences: (source: string, group: SubjectReferenceGroup, rename: SubjectRename) => SubjectRewriteResult;
86
+ /**
87
+ * Local ids of the architecture states a document declares. States share the
88
+ * `document#local` spelling with subjects but not the id space, so a rename
89
+ * whose old or new id collides with one cannot be re-pointed unambiguously and
90
+ * is refused instead.
91
+ */
92
+ export declare const declaredStateIds: (source: string) => readonly string[];
@@ -0,0 +1,226 @@
1
+ import { isAlias, isMap, isScalar, isSeq, parseDocument } from 'yaml';
2
+ export const SUBJECT_REFERENCE_POSITIONS = [
3
+ { group: 'document', path: ['concepts', '*', 'id'], form: 'declaration' },
4
+ { group: 'document', path: ['concepts', '*', 'owner'], form: 'reference' },
5
+ {
6
+ group: 'document',
7
+ path: ['concepts', '*', 'distinctFrom', '*'],
8
+ form: 'reference',
9
+ },
10
+ {
11
+ group: 'document',
12
+ path: ['concepts', '*', 'supersedes', '*'],
13
+ form: 'reference',
14
+ },
15
+ {
16
+ group: 'document',
17
+ path: ['concepts', '*', 'constraints', '*', 'ref'],
18
+ form: 'reference',
19
+ },
20
+ {
21
+ group: 'document',
22
+ path: ['concepts', '*', 'references', '*', 'ref'],
23
+ form: 'reference',
24
+ },
25
+ {
26
+ group: 'document',
27
+ path: ['concepts', '*', 'attestations', '*', 'by'],
28
+ form: 'reference',
29
+ },
30
+ {
31
+ group: 'document',
32
+ path: ['relationships', '*', 'id'],
33
+ form: 'declaration',
34
+ },
35
+ {
36
+ group: 'document',
37
+ path: ['relationships', '*', 'from'],
38
+ form: 'reference',
39
+ },
40
+ { group: 'document', path: ['relationships', '*', 'to'], form: 'reference' },
41
+ {
42
+ group: 'document',
43
+ path: ['relationships', '*', 'references', '*', 'ref'],
44
+ form: 'reference',
45
+ },
46
+ {
47
+ group: 'projection',
48
+ path: ['query', 'subjects', '*'],
49
+ form: 'qualified',
50
+ },
51
+ { group: 'projection', path: ['query', 'owners', '*'], form: 'qualified' },
52
+ {
53
+ group: 'projection',
54
+ path: ['query', 'constraints', '*'],
55
+ form: 'qualified',
56
+ },
57
+ {
58
+ group: 'evidence',
59
+ path: ['observations', '*', 'subject'],
60
+ form: 'qualified',
61
+ },
62
+ {
63
+ group: 'evidence',
64
+ path: ['observations', '*', 'claim'],
65
+ form: 'qualified',
66
+ },
67
+ {
68
+ group: 'adapter-mapping',
69
+ path: ['mappings', '*', 'native'],
70
+ form: 'qualified',
71
+ },
72
+ ];
73
+ /**
74
+ * Positions that carry reference *syntax* but are not subject addresses, with
75
+ * the reason. Kept as data because the schema-derived completeness test asserts
76
+ * that the two lists together account for every reference-typed position: an
77
+ * omission has to be argued for here rather than forgotten.
78
+ */
79
+ export const EXCLUDED_REFERENCE_POSITIONS = [
80
+ {
81
+ group: 'document',
82
+ path: ['states', '*', 'after'],
83
+ reason: 'architecture state address, a separate id space',
84
+ },
85
+ {
86
+ group: 'document',
87
+ path: ['concepts', '*', 'presentIn', '*'],
88
+ reason: 'architecture state address, a separate id space',
89
+ },
90
+ {
91
+ group: 'document',
92
+ path: ['relationships', '*', 'presentIn', '*'],
93
+ reason: 'architecture state address, a separate id space',
94
+ },
95
+ {
96
+ group: 'projection',
97
+ path: ['query', 'states', '*'],
98
+ reason: 'architecture state address, a separate id space',
99
+ },
100
+ {
101
+ group: 'document',
102
+ path: ['concepts', '*', 'constraints', '*', 'expects', 'key'],
103
+ reason: 'observation key: provider vocabulary, not an address',
104
+ },
105
+ {
106
+ group: 'evidence',
107
+ path: ['observations', '*', 'key'],
108
+ reason: 'observation key: provider vocabulary, not an address',
109
+ },
110
+ ];
111
+ const qualify = (documentId, value) => value.includes('#') ? value : `${documentId}#${value}`;
112
+ const localOf = (qualified) => qualified.slice(qualified.indexOf('#') + 1);
113
+ /** Walks one position pattern and collects what it reaches. */
114
+ const collect = (node, path, pointer, form, documentId, source, hits, aliases) => {
115
+ if (path.length === 0) {
116
+ if (isAlias(node)) {
117
+ aliases.push(pointer);
118
+ return;
119
+ }
120
+ if (!isScalar(node) || typeof node.value !== 'string')
121
+ return;
122
+ const range = node.range;
123
+ if (range === null || range === undefined)
124
+ return;
125
+ const [start, end] = range;
126
+ const aspect = node.value.indexOf('~');
127
+ const base = aspect === -1 ? node.value : node.value.slice(0, aspect);
128
+ hits.push({
129
+ pointer,
130
+ address: form === 'qualified' ? base : qualify(documentId, base),
131
+ form,
132
+ start,
133
+ end,
134
+ raw: source.slice(start, end),
135
+ });
136
+ return;
137
+ }
138
+ const [segment, ...rest] = path;
139
+ if (segment === '*') {
140
+ if (!isSeq(node))
141
+ return;
142
+ for (const [index, item] of node.items.entries()) {
143
+ collect(item, rest, `${pointer}/${index}`, form, documentId, source, hits, aliases);
144
+ }
145
+ return;
146
+ }
147
+ if (!isMap(node))
148
+ return;
149
+ collect(node.get(segment, true), rest, `${pointer}/${segment}`, form, documentId, source, hits, aliases);
150
+ };
151
+ /**
152
+ * Every subject address a file of this group holds. Pure read: the parse is
153
+ * thrown away, so callers stay free to splice the original bytes.
154
+ */
155
+ export const scanSubjectReferences = (source, group) => {
156
+ const root = parseDocument(source).contents;
157
+ const id = isMap(root) ? root.get('id', true) : undefined;
158
+ const documentId = isScalar(id) && typeof id.value === 'string' ? id.value : '';
159
+ const hits = [];
160
+ const aliases = [];
161
+ for (const position of SUBJECT_REFERENCE_POSITIONS) {
162
+ if (position.group !== group)
163
+ continue;
164
+ collect(root, position.path, '', position.form, documentId, source, hits, aliases);
165
+ }
166
+ return { documentId, hits, aliases };
167
+ };
168
+ /** The quote character a scalar was written with, `''` when it was plain. */
169
+ const quoteOf = (raw) => (raw.startsWith("'") && raw.endsWith("'")) ||
170
+ (raw.startsWith('"') && raw.endsWith('"'))
171
+ ? raw[0]
172
+ : '';
173
+ /**
174
+ * Re-points every reference to `rename.from` in one file. Only the matched
175
+ * scalars' own bytes change - nothing is re-rendered, so byte identity holds
176
+ * everywhere else, a bare reference stays bare, a qualified one stays
177
+ * qualified, a `~aspect` suffix survives, and the original quoting is kept.
178
+ */
179
+ export const rewriteSubjectReferences = (source, group, rename) => {
180
+ const scan = scanSubjectReferences(source, group);
181
+ if (scan.aliases.length > 0)
182
+ return { ok: false, aliases: scan.aliases };
183
+ const matches = scan.hits.filter((hit) => hit.address === rename.from);
184
+ if (matches.length === 0)
185
+ return { ok: true, source, moved: [] };
186
+ let rewritten = source;
187
+ // Back to front, so an earlier splice never shifts a later offset.
188
+ const ordered = [...matches].sort((left, right) => right.start - left.start);
189
+ for (const hit of ordered) {
190
+ const quote = quoteOf(hit.raw);
191
+ const written = quote === '' ? hit.raw : hit.raw.slice(1, -1);
192
+ const aspect = written.indexOf('~');
193
+ const suffix = aspect === -1 ? '' : written.slice(aspect);
194
+ const moved = written.includes('#') ? rename.to : localOf(rename.to);
195
+ rewritten =
196
+ rewritten.slice(0, hit.start) +
197
+ `${quote}${moved}${suffix}${quote}` +
198
+ rewritten.slice(hit.end);
199
+ }
200
+ return {
201
+ ok: true,
202
+ source: rewritten,
203
+ moved: matches.map((hit) => hit.pointer),
204
+ };
205
+ };
206
+ /**
207
+ * Local ids of the architecture states a document declares. States share the
208
+ * `document#local` spelling with subjects but not the id space, so a rename
209
+ * whose old or new id collides with one cannot be re-pointed unambiguously and
210
+ * is refused instead.
211
+ */
212
+ export const declaredStateIds = (source) => {
213
+ const root = parseDocument(source).contents;
214
+ const states = isMap(root) ? root.get('states', true) : undefined;
215
+ if (!isSeq(states))
216
+ return [];
217
+ const ids = [];
218
+ for (const item of states.items) {
219
+ if (!isMap(item))
220
+ continue;
221
+ const id = item.get('id', true);
222
+ if (isScalar(id) && typeof id.value === 'string')
223
+ ids.push(id.value);
224
+ }
225
+ return ids;
226
+ };