@usefragments/core 1.0.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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,446 @@
1
+ /**
2
+ * ComponentGraphEngine — query engine for the design-system relationship graph.
3
+ *
4
+ * Provides BFS-based traversal, impact analysis, composition trees,
5
+ * alternative discovery, and health metrics. Operates on an in-memory
6
+ * adjacency list built from GraphEdge[].
7
+ */
8
+
9
+ import type {
10
+ ComponentNode,
11
+ GraphEdge,
12
+ GraphEdgeType,
13
+ GraphHealth,
14
+ ComponentGraph,
15
+ ImpactResult,
16
+ PathResult,
17
+ NeighborResult,
18
+ CompositionTree,
19
+ } from './types.js';
20
+
21
+ export class ComponentGraphEngine {
22
+ private nodes: Map<string, ComponentNode>;
23
+ private outgoing: Map<string, GraphEdge[]>;
24
+ private incoming: Map<string, GraphEdge[]>;
25
+ private edges: GraphEdge[];
26
+ private blockIndex: Map<string, string[]>;
27
+ private health: GraphHealth;
28
+
29
+ constructor(graph: ComponentGraph, blocks?: Record<string, { components: string[] }>) {
30
+ this.nodes = new Map();
31
+ this.outgoing = new Map();
32
+ this.incoming = new Map();
33
+ this.edges = graph.edges;
34
+ this.health = graph.health;
35
+ this.blockIndex = new Map();
36
+
37
+ for (const node of graph.nodes) {
38
+ this.nodes.set(node.name, node);
39
+ this.outgoing.set(node.name, []);
40
+ this.incoming.set(node.name, []);
41
+ }
42
+
43
+ for (const edge of graph.edges) {
44
+ const out = this.outgoing.get(edge.source);
45
+ if (out) out.push(edge);
46
+ else this.outgoing.set(edge.source, [edge]);
47
+
48
+ const inc = this.incoming.get(edge.target);
49
+ if (inc) inc.push(edge);
50
+ else this.incoming.set(edge.target, [edge]);
51
+ }
52
+
53
+ // Build block index: component → block names
54
+ if (blocks) {
55
+ for (const [blockName, block] of Object.entries(blocks)) {
56
+ for (const comp of block.components) {
57
+ const existing = this.blockIndex.get(comp);
58
+ if (existing) existing.push(blockName);
59
+ else this.blockIndex.set(comp, [blockName]);
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // -------------------------------------------------------------------------
66
+ // Core queries
67
+ // -------------------------------------------------------------------------
68
+
69
+ /** Get outgoing edges from a component, optionally filtered by edge type */
70
+ dependencies(component: string, edgeTypes?: GraphEdgeType[]): GraphEdge[] {
71
+ const edges = this.outgoing.get(component) ?? [];
72
+ if (!edgeTypes || edgeTypes.length === 0) return edges;
73
+ return edges.filter(e => edgeTypes.includes(e.type));
74
+ }
75
+
76
+ /** Get incoming edges to a component, optionally filtered by edge type */
77
+ dependents(component: string, edgeTypes?: GraphEdgeType[]): GraphEdge[] {
78
+ const edges = this.incoming.get(component) ?? [];
79
+ if (!edgeTypes || edgeTypes.length === 0) return edges;
80
+ return edges.filter(e => edgeTypes.includes(e.type));
81
+ }
82
+
83
+ /** BFS transitive closure — what's affected if this component changes */
84
+ impact(component: string, maxDepth = 3): ImpactResult {
85
+ const affected: ImpactResult['affected'] = [];
86
+ const visited = new Set<string>([component]);
87
+ const queue: Array<{ name: string; depth: number; path: string[] }> = [
88
+ { name: component, depth: 0, path: [component] },
89
+ ];
90
+
91
+ while (queue.length > 0) {
92
+ const current = queue.shift()!;
93
+ if (current.depth >= maxDepth) continue;
94
+
95
+ // Components that depend on current (incoming edges = dependents)
96
+ const deps = this.incoming.get(current.name) ?? [];
97
+ for (const edge of deps) {
98
+ if (visited.has(edge.source)) continue;
99
+ visited.add(edge.source);
100
+ const newPath = [...current.path, edge.source];
101
+ affected.push({
102
+ component: edge.source,
103
+ depth: current.depth + 1,
104
+ path: newPath,
105
+ edgeType: edge.type,
106
+ });
107
+ queue.push({ name: edge.source, depth: current.depth + 1, path: newPath });
108
+ }
109
+ }
110
+
111
+ // Find affected blocks
112
+ const affectedComponents = new Set([component, ...affected.map(a => a.component)]);
113
+ const affectedBlocks = new Set<string>();
114
+ for (const comp of affectedComponents) {
115
+ const blocks = this.blockIndex.get(comp);
116
+ if (blocks) {
117
+ for (const b of blocks) affectedBlocks.add(b);
118
+ }
119
+ }
120
+
121
+ return {
122
+ component,
123
+ affected,
124
+ affectedBlocks: [...affectedBlocks],
125
+ totalAffected: affected.length,
126
+ };
127
+ }
128
+
129
+ /** BFS shortest path between two components (undirected) */
130
+ path(from: string, to: string): PathResult {
131
+ if (from === to) {
132
+ return { found: true, path: [from], edges: [] };
133
+ }
134
+
135
+ const visited = new Set<string>([from]);
136
+ const queue: Array<{ name: string; path: string[]; edges: GraphEdge[] }> = [
137
+ { name: from, path: [from], edges: [] },
138
+ ];
139
+
140
+ while (queue.length > 0) {
141
+ const current = queue.shift()!;
142
+
143
+ // Check both outgoing and incoming edges (undirected traversal)
144
+ const allEdges = [
145
+ ...(this.outgoing.get(current.name) ?? []),
146
+ ...(this.incoming.get(current.name) ?? []),
147
+ ];
148
+
149
+ for (const edge of allEdges) {
150
+ const neighbor = edge.source === current.name ? edge.target : edge.source;
151
+ if (visited.has(neighbor)) continue;
152
+ visited.add(neighbor);
153
+
154
+ const newPath = [...current.path, neighbor];
155
+ const newEdges = [...current.edges, edge];
156
+
157
+ if (neighbor === to) {
158
+ return { found: true, path: newPath, edges: newEdges };
159
+ }
160
+
161
+ queue.push({ name: neighbor, path: newPath, edges: newEdges });
162
+ }
163
+ }
164
+
165
+ return { found: false, path: [], edges: [] };
166
+ }
167
+
168
+ /** Connected components via BFS on undirected projection */
169
+ islands(): string[][] {
170
+ const visited = new Set<string>();
171
+ const components: string[][] = [];
172
+
173
+ for (const nodeName of this.nodes.keys()) {
174
+ if (visited.has(nodeName)) continue;
175
+
176
+ const island: string[] = [];
177
+ const queue = [nodeName];
178
+ visited.add(nodeName);
179
+
180
+ while (queue.length > 0) {
181
+ const current = queue.shift()!;
182
+ island.push(current);
183
+
184
+ const allEdges = [
185
+ ...(this.outgoing.get(current) ?? []),
186
+ ...(this.incoming.get(current) ?? []),
187
+ ];
188
+
189
+ for (const edge of allEdges) {
190
+ const neighbor = edge.source === current ? edge.target : edge.source;
191
+ if (!visited.has(neighbor) && this.nodes.has(neighbor)) {
192
+ visited.add(neighbor);
193
+ queue.push(neighbor);
194
+ }
195
+ }
196
+ }
197
+
198
+ components.push(island.sort());
199
+ }
200
+
201
+ return components.sort((a, b) => b.length - a.length);
202
+ }
203
+
204
+ /** All components reachable within N hops (undirected) */
205
+ neighbors(component: string, maxHops = 1): NeighborResult {
206
+ const neighbors: NeighborResult['neighbors'] = [];
207
+ const visited = new Set<string>([component]);
208
+ const queue: Array<{ name: string; hops: number }> = [
209
+ { name: component, hops: 0 },
210
+ ];
211
+
212
+ while (queue.length > 0) {
213
+ const current = queue.shift()!;
214
+ if (current.hops >= maxHops) continue;
215
+
216
+ const allEdges = [
217
+ ...(this.outgoing.get(current.name) ?? []),
218
+ ...(this.incoming.get(current.name) ?? []),
219
+ ];
220
+
221
+ for (const edge of allEdges) {
222
+ const neighbor = edge.source === current.name ? edge.target : edge.source;
223
+ if (visited.has(neighbor)) continue;
224
+ visited.add(neighbor);
225
+
226
+ neighbors.push({
227
+ component: neighbor,
228
+ hops: current.hops + 1,
229
+ edgeType: edge.type,
230
+ });
231
+
232
+ queue.push({ name: neighbor, hops: current.hops + 1 });
233
+ }
234
+ }
235
+
236
+ return { component, neighbors };
237
+ }
238
+
239
+ // -------------------------------------------------------------------------
240
+ // Design-system queries
241
+ // -------------------------------------------------------------------------
242
+
243
+ /** Get the composition tree for a compound component */
244
+ composition(component: string): CompositionTree {
245
+ const node = this.nodes.get(component);
246
+
247
+ // Find sub-components from node data
248
+ const subComponents = node?.subComponents ?? [];
249
+
250
+ // Find children from outgoing parent-of edges
251
+ const parentEdges = (this.outgoing.get(component) ?? [])
252
+ .filter(e => e.type === 'parent-of');
253
+ const children = parentEdges.map(e => e.target);
254
+
255
+ // Find parent (incoming parent-of edges)
256
+ const childEdges = (this.incoming.get(component) ?? [])
257
+ .filter(e => e.type === 'parent-of');
258
+ const parent = childEdges.length > 0 ? childEdges[0].source : undefined;
259
+
260
+ // Find siblings (other targets of the same parent)
261
+ const siblings: string[] = [];
262
+ if (parent) {
263
+ const parentOut = (this.outgoing.get(parent) ?? [])
264
+ .filter(e => e.type === 'parent-of');
265
+ for (const edge of parentOut) {
266
+ if (edge.target !== component) {
267
+ siblings.push(edge.target);
268
+ }
269
+ }
270
+ }
271
+
272
+ // Also check sibling-of edges
273
+ const siblingEdges = [
274
+ ...(this.outgoing.get(component) ?? []).filter(e => e.type === 'sibling-of'),
275
+ ...(this.incoming.get(component) ?? []).filter(e => e.type === 'sibling-of'),
276
+ ];
277
+ for (const edge of siblingEdges) {
278
+ const sib = edge.source === component ? edge.target : edge.source;
279
+ if (!siblings.includes(sib)) siblings.push(sib);
280
+ }
281
+
282
+ return {
283
+ component,
284
+ compositionPattern: node?.compositionPattern,
285
+ subComponents,
286
+ children,
287
+ parent,
288
+ siblings,
289
+ blocks: this.blockIndex.get(component) ?? [],
290
+ };
291
+ }
292
+
293
+ /** Get alternative components (deduplicated for bidirectional edges) */
294
+ alternatives(component: string): Array<{ component: string; note?: string }> {
295
+ const seen = new Set<string>();
296
+ const alts: Array<{ component: string; note?: string }> = [];
297
+
298
+ // Outgoing alternative-to edges
299
+ for (const edge of this.outgoing.get(component) ?? []) {
300
+ if (edge.type === 'alternative-to' && !seen.has(edge.target)) {
301
+ seen.add(edge.target);
302
+ alts.push({ component: edge.target, note: edge.note });
303
+ }
304
+ }
305
+
306
+ // Incoming alternative-to edges (skip if already seen from outgoing)
307
+ for (const edge of this.incoming.get(component) ?? []) {
308
+ if (edge.type === 'alternative-to' && !seen.has(edge.source)) {
309
+ seen.add(edge.source);
310
+ alts.push({ component: edge.source, note: edge.note });
311
+ }
312
+ }
313
+
314
+ return alts;
315
+ }
316
+
317
+ /** Get blocks that use a component */
318
+ blocksUsing(component: string): string[] {
319
+ return this.blockIndex.get(component) ?? [];
320
+ }
321
+
322
+ /** Extract an induced subgraph for a set of components */
323
+ subgraph(components: string[]): ComponentGraph {
324
+ const componentSet = new Set(components);
325
+ const nodes = components
326
+ .map(name => this.nodes.get(name))
327
+ .filter((n): n is ComponentNode => n !== undefined);
328
+
329
+ const edges = this.edges.filter(
330
+ e => componentSet.has(e.source) && componentSet.has(e.target)
331
+ );
332
+
333
+ return {
334
+ nodes,
335
+ edges,
336
+ health: computeHealthFromData(nodes, edges, this.blockIndex),
337
+ };
338
+ }
339
+
340
+ /** Return precomputed health metrics */
341
+ getHealth(): GraphHealth {
342
+ return this.health;
343
+ }
344
+
345
+ /** Get a single node by name */
346
+ getNode(name: string): ComponentNode | undefined {
347
+ return this.nodes.get(name);
348
+ }
349
+
350
+ /** Check if a component exists in the graph */
351
+ hasNode(name: string): boolean {
352
+ return this.nodes.has(name);
353
+ }
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Health computation helper (also used by graph-extractor)
358
+ // ---------------------------------------------------------------------------
359
+
360
+ export function computeHealthFromData(
361
+ nodes: ComponentNode[],
362
+ edges: GraphEdge[],
363
+ blockIndex?: Map<string, string[]>
364
+ ): GraphHealth {
365
+ const nodeNames = new Set(nodes.map(n => n.name));
366
+ const degreeMap = new Map<string, number>();
367
+
368
+ for (const name of nodeNames) {
369
+ degreeMap.set(name, 0);
370
+ }
371
+
372
+ for (const edge of edges) {
373
+ degreeMap.set(edge.source, (degreeMap.get(edge.source) ?? 0) + 1);
374
+ degreeMap.set(edge.target, (degreeMap.get(edge.target) ?? 0) + 1);
375
+ }
376
+
377
+ // Orphans: zero degree
378
+ const orphans: string[] = [];
379
+ for (const [name, degree] of degreeMap) {
380
+ if (degree === 0) orphans.push(name);
381
+ }
382
+
383
+ // Hubs: top 10 by degree
384
+ const hubs = [...degreeMap.entries()]
385
+ .map(([name, degree]) => ({ name, degree }))
386
+ .sort((a, b) => b.degree - a.degree)
387
+ .slice(0, 10);
388
+
389
+ // Composition coverage
390
+ let inBlock = 0;
391
+ if (blockIndex) {
392
+ for (const name of nodeNames) {
393
+ if ((blockIndex.get(name) ?? []).length > 0) inBlock++;
394
+ }
395
+ }
396
+ const compositionCoverage = nodeNames.size > 0
397
+ ? Math.round((inBlock / nodeNames.size) * 100)
398
+ : 0;
399
+
400
+ // Connected components (BFS on undirected projection)
401
+ const adjacency = new Map<string, Set<string>>();
402
+ for (const name of nodeNames) {
403
+ adjacency.set(name, new Set());
404
+ }
405
+ for (const edge of edges) {
406
+ adjacency.get(edge.source)?.add(edge.target);
407
+ adjacency.get(edge.target)?.add(edge.source);
408
+ }
409
+
410
+ const visited = new Set<string>();
411
+ const connectedComponents: string[][] = [];
412
+ for (const name of nodeNames) {
413
+ if (visited.has(name)) continue;
414
+ const island: string[] = [];
415
+ const queue = [name];
416
+ visited.add(name);
417
+ while (queue.length > 0) {
418
+ const current = queue.shift()!;
419
+ island.push(current);
420
+ for (const neighbor of adjacency.get(current) ?? []) {
421
+ if (!visited.has(neighbor)) {
422
+ visited.add(neighbor);
423
+ queue.push(neighbor);
424
+ }
425
+ }
426
+ }
427
+ connectedComponents.push(island.sort());
428
+ }
429
+ connectedComponents.sort((a, b) => b.length - a.length);
430
+
431
+ // Average degree
432
+ const totalDegree = [...degreeMap.values()].reduce((sum, d) => sum + d, 0);
433
+ const averageDegree = nodeNames.size > 0
434
+ ? Math.round((totalDegree / nodeNames.size) * 100) / 100
435
+ : 0;
436
+
437
+ return {
438
+ orphans: orphans.sort(),
439
+ hubs,
440
+ compositionCoverage,
441
+ connectedComponents,
442
+ averageDegree,
443
+ nodeCount: nodeNames.size,
444
+ edgeCount: edges.length,
445
+ };
446
+ }