@oscharko-dev/keiko-quality-intelligence 0.2.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 (203) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/__tests__/_fixtureLoader.d.ts +9 -0
  3. package/dist/__tests__/_fixtureLoader.d.ts.map +1 -0
  4. package/dist/__tests__/_fixtureLoader.js +75 -0
  5. package/dist/domain/assertions.d.ts +61 -0
  6. package/dist/domain/assertions.d.ts.map +1 -0
  7. package/dist/domain/assertions.js +134 -0
  8. package/dist/domain/coverageRelevance.d.ts +73 -0
  9. package/dist/domain/coverageRelevance.d.ts.map +1 -0
  10. package/dist/domain/coverageRelevance.js +155 -0
  11. package/dist/domain/deduplication.d.ts +17 -0
  12. package/dist/domain/deduplication.d.ts.map +1 -0
  13. package/dist/domain/deduplication.js +95 -0
  14. package/dist/domain/figma/a11yBaseline.d.ts +17 -0
  15. package/dist/domain/figma/a11yBaseline.d.ts.map +1 -0
  16. package/dist/domain/figma/a11yBaseline.js +218 -0
  17. package/dist/domain/figma/cleanToScreenIr.d.ts +3 -0
  18. package/dist/domain/figma/cleanToScreenIr.d.ts.map +1 -0
  19. package/dist/domain/figma/cleanToScreenIr.js +62 -0
  20. package/dist/domain/figma/codeTargetAdapter.d.ts +30 -0
  21. package/dist/domain/figma/codeTargetAdapter.d.ts.map +1 -0
  22. package/dist/domain/figma/codeTargetAdapter.js +27 -0
  23. package/dist/domain/figma/color.d.ts +31 -0
  24. package/dist/domain/figma/color.d.ts.map +1 -0
  25. package/dist/domain/figma/color.js +99 -0
  26. package/dist/domain/figma/emissionPlan.d.ts +56 -0
  27. package/dist/domain/figma/emissionPlan.d.ts.map +1 -0
  28. package/dist/domain/figma/emissionPlan.js +87 -0
  29. package/dist/domain/figma/htmlCssAdapter.d.ts +13 -0
  30. package/dist/domain/figma/htmlCssAdapter.d.ts.map +1 -0
  31. package/dist/domain/figma/htmlCssAdapter.js +452 -0
  32. package/dist/domain/figma/index.d.ts +19 -0
  33. package/dist/domain/figma/index.d.ts.map +1 -0
  34. package/dist/domain/figma/index.js +31 -0
  35. package/dist/domain/figma/irTypes.d.ts +156 -0
  36. package/dist/domain/figma/irTypes.d.ts.map +1 -0
  37. package/dist/domain/figma/irTypes.js +8 -0
  38. package/dist/domain/figma/links.d.ts +6 -0
  39. package/dist/domain/figma/links.d.ts.map +1 -0
  40. package/dist/domain/figma/links.js +102 -0
  41. package/dist/domain/figma/navGraph.d.ts +74 -0
  42. package/dist/domain/figma/navGraph.d.ts.map +1 -0
  43. package/dist/domain/figma/navGraph.js +315 -0
  44. package/dist/domain/figma/normalize.d.ts +7 -0
  45. package/dist/domain/figma/normalize.d.ts.map +1 -0
  46. package/dist/domain/figma/normalize.js +252 -0
  47. package/dist/domain/figma/prune.d.ts +15 -0
  48. package/dist/domain/figma/prune.d.ts.map +1 -0
  49. package/dist/domain/figma/prune.js +65 -0
  50. package/dist/domain/figma/screenDetect.d.ts +8 -0
  51. package/dist/domain/figma/screenDetect.d.ts.map +1 -0
  52. package/dist/domain/figma/screenDetect.js +35 -0
  53. package/dist/domain/figma/screenIrTestBaseline.d.ts +52 -0
  54. package/dist/domain/figma/screenIrTestBaseline.d.ts.map +1 -0
  55. package/dist/domain/figma/screenIrTestBaseline.js +326 -0
  56. package/dist/domain/figma/semanticNaming.d.ts +24 -0
  57. package/dist/domain/figma/semanticNaming.d.ts.map +1 -0
  58. package/dist/domain/figma/semanticNaming.js +67 -0
  59. package/dist/domain/figma/sourceNode.d.ts +24 -0
  60. package/dist/domain/figma/sourceNode.d.ts.map +1 -0
  61. package/dist/domain/figma/sourceNode.js +26 -0
  62. package/dist/domain/figma/tokens.d.ts +11 -0
  63. package/dist/domain/figma/tokens.d.ts.map +1 -0
  64. package/dist/domain/figma/tokens.js +148 -0
  65. package/dist/domain/figma/visionAugmentation.d.ts +14 -0
  66. package/dist/domain/figma/visionAugmentation.d.ts.map +1 -0
  67. package/dist/domain/figma/visionAugmentation.js +48 -0
  68. package/dist/domain/intentDerivation.d.ts +21 -0
  69. package/dist/domain/intentDerivation.d.ts.map +1 -0
  70. package/dist/domain/intentDerivation.js +126 -0
  71. package/dist/domain/policyProfile.d.ts +37 -0
  72. package/dist/domain/policyProfile.d.ts.map +1 -0
  73. package/dist/domain/policyProfile.js +94 -0
  74. package/dist/domain/requirementExcerpt.d.ts +9 -0
  75. package/dist/domain/requirementExcerpt.d.ts.map +1 -0
  76. package/dist/domain/requirementExcerpt.js +39 -0
  77. package/dist/domain/staleness.d.ts +56 -0
  78. package/dist/domain/staleness.d.ts.map +1 -0
  79. package/dist/domain/staleness.js +313 -0
  80. package/dist/domain/testDesignModel.d.ts +38 -0
  81. package/dist/domain/testDesignModel.d.ts.map +1 -0
  82. package/dist/domain/testDesignModel.js +264 -0
  83. package/dist/domain/testQualityRubric.d.ts +20 -0
  84. package/dist/domain/testQualityRubric.d.ts.map +1 -0
  85. package/dist/domain/testQualityRubric.js +38 -0
  86. package/dist/domain/validation.d.ts +7 -0
  87. package/dist/domain/validation.d.ts.map +1 -0
  88. package/dist/domain/validation.js +145 -0
  89. package/dist/export/adapters/alm.d.ts +4 -0
  90. package/dist/export/adapters/alm.d.ts.map +1 -0
  91. package/dist/export/adapters/alm.js +75 -0
  92. package/dist/export/adapters/csv.d.ts +5 -0
  93. package/dist/export/adapters/csv.d.ts.map +1 -0
  94. package/dist/export/adapters/csv.js +55 -0
  95. package/dist/export/adapters/index.d.ts +13 -0
  96. package/dist/export/adapters/index.d.ts.map +1 -0
  97. package/dist/export/adapters/index.js +15 -0
  98. package/dist/export/adapters/jira.d.ts +5 -0
  99. package/dist/export/adapters/jira.d.ts.map +1 -0
  100. package/dist/export/adapters/jira.js +79 -0
  101. package/dist/export/adapters/json.d.ts +3 -0
  102. package/dist/export/adapters/json.d.ts.map +1 -0
  103. package/dist/export/adapters/json.js +54 -0
  104. package/dist/export/adapters/markdown.d.ts +3 -0
  105. package/dist/export/adapters/markdown.d.ts.map +1 -0
  106. package/dist/export/adapters/markdown.js +88 -0
  107. package/dist/export/adapters/plaintext.d.ts +3 -0
  108. package/dist/export/adapters/plaintext.d.ts.map +1 -0
  109. package/dist/export/adapters/plaintext.js +65 -0
  110. package/dist/export/adapters/polarion.d.ts +4 -0
  111. package/dist/export/adapters/polarion.d.ts.map +1 -0
  112. package/dist/export/adapters/polarion.js +67 -0
  113. package/dist/export/adapters/qtest.d.ts +4 -0
  114. package/dist/export/adapters/qtest.d.ts.map +1 -0
  115. package/dist/export/adapters/qtest.js +78 -0
  116. package/dist/export/adapters/qualityCenter.d.ts +3 -0
  117. package/dist/export/adapters/qualityCenter.d.ts.map +1 -0
  118. package/dist/export/adapters/qualityCenter.js +56 -0
  119. package/dist/export/adapters/spreadsheetSafeCsv.d.ts +36 -0
  120. package/dist/export/adapters/spreadsheetSafeCsv.d.ts.map +1 -0
  121. package/dist/export/adapters/spreadsheetSafeCsv.js +157 -0
  122. package/dist/export/adapters/traceability.d.ts +34 -0
  123. package/dist/export/adapters/traceability.d.ts.map +1 -0
  124. package/dist/export/adapters/traceability.js +142 -0
  125. package/dist/export/adapters/xray.d.ts +4 -0
  126. package/dist/export/adapters/xray.d.ts.map +1 -0
  127. package/dist/export/adapters/xray.js +72 -0
  128. package/dist/export/formats.d.ts +29 -0
  129. package/dist/export/formats.d.ts.map +1 -0
  130. package/dist/export/formats.js +34 -0
  131. package/dist/export/index.d.ts +4 -0
  132. package/dist/export/index.d.ts.map +1 -0
  133. package/dist/export/index.js +10 -0
  134. package/dist/export/serialize.d.ts +17 -0
  135. package/dist/export/serialize.d.ts.map +1 -0
  136. package/dist/export/serialize.js +56 -0
  137. package/dist/export/textSafety.d.ts +15 -0
  138. package/dist/export/textSafety.d.ts.map +1 -0
  139. package/dist/export/textSafety.js +30 -0
  140. package/dist/generation/candidateBounds.d.ts +10 -0
  141. package/dist/generation/candidateBounds.d.ts.map +1 -0
  142. package/dist/generation/candidateBounds.js +14 -0
  143. package/dist/generation/index.d.ts +4 -0
  144. package/dist/generation/index.d.ts.map +1 -0
  145. package/dist/generation/index.js +20 -0
  146. package/dist/generation/parseGeneratedCandidates.d.ts +27 -0
  147. package/dist/generation/parseGeneratedCandidates.d.ts.map +1 -0
  148. package/dist/generation/parseGeneratedCandidates.js +253 -0
  149. package/dist/generation/prompt.d.ts +16 -0
  150. package/dist/generation/prompt.d.ts.map +1 -0
  151. package/dist/generation/prompt.js +151 -0
  152. package/dist/generation/requirementsIngestion.d.ts +21 -0
  153. package/dist/generation/requirementsIngestion.d.ts.map +1 -0
  154. package/dist/generation/requirementsIngestion.js +70 -0
  155. package/dist/hardening/index.d.ts +6 -0
  156. package/dist/hardening/index.d.ts.map +1 -0
  157. package/dist/hardening/index.js +8 -0
  158. package/dist/hardening/oversizeGuards.d.ts +21 -0
  159. package/dist/hardening/oversizeGuards.d.ts.map +1 -0
  160. package/dist/hardening/oversizeGuards.js +35 -0
  161. package/dist/hardening/pathSafety.d.ts +19 -0
  162. package/dist/hardening/pathSafety.d.ts.map +1 -0
  163. package/dist/hardening/pathSafety.js +61 -0
  164. package/dist/hardening/promptInjectionScrub.d.ts +17 -0
  165. package/dist/hardening/promptInjectionScrub.d.ts.map +1 -0
  166. package/dist/hardening/promptInjectionScrub.js +72 -0
  167. package/dist/index.d.ts +22 -0
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +44 -0
  170. package/dist/ingestion/adfParser.d.ts +61 -0
  171. package/dist/ingestion/adfParser.d.ts.map +1 -0
  172. package/dist/ingestion/adfParser.js +262 -0
  173. package/dist/ingestion/index.d.ts +6 -0
  174. package/dist/ingestion/index.d.ts.map +1 -0
  175. package/dist/ingestion/index.js +10 -0
  176. package/dist/ingestion/sourceMixPlanning.d.ts +36 -0
  177. package/dist/ingestion/sourceMixPlanning.d.ts.map +1 -0
  178. package/dist/ingestion/sourceMixPlanning.js +65 -0
  179. package/dist/ingestion/sourceReconciliation.d.ts +39 -0
  180. package/dist/ingestion/sourceReconciliation.d.ts.map +1 -0
  181. package/dist/ingestion/sourceReconciliation.js +74 -0
  182. package/dist/ingestion/untrustedContentNormalisation.d.ts +23 -0
  183. package/dist/ingestion/untrustedContentNormalisation.d.ts.map +1 -0
  184. package/dist/ingestion/untrustedContentNormalisation.js +121 -0
  185. package/dist/ingestion/workspaceAdapter.d.ts +55 -0
  186. package/dist/ingestion/workspaceAdapter.d.ts.map +1 -0
  187. package/dist/ingestion/workspaceAdapter.js +113 -0
  188. package/dist/review/auditEvents.d.ts +61 -0
  189. package/dist/review/auditEvents.d.ts.map +1 -0
  190. package/dist/review/auditEvents.js +50 -0
  191. package/dist/review/fourEyes.d.ts +24 -0
  192. package/dist/review/fourEyes.d.ts.map +1 -0
  193. package/dist/review/fourEyes.js +45 -0
  194. package/dist/review/index.d.ts +5 -0
  195. package/dist/review/index.d.ts.map +1 -0
  196. package/dist/review/index.js +14 -0
  197. package/dist/review/lifecyclePolicy.d.ts +21 -0
  198. package/dist/review/lifecyclePolicy.d.ts.map +1 -0
  199. package/dist/review/lifecyclePolicy.js +38 -0
  200. package/dist/review/stateMachine.d.ts +28 -0
  201. package/dist/review/stateMachine.d.ts.map +1 -0
  202. package/dist/review/stateMachine.js +71 -0
  203. package/package.json +31 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"irTypes.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/irTypes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ // Screen-IR, design-token, and inter-screen-link types (Epic #750, Issue #752).
2
+ //
3
+ // The IR is the lean structural ground truth a scoped Figma board reduces to: per-screen kept-node
4
+ // trees plus deduped design tokens and the raw inter-screen transitions. These types are the
5
+ // import surface for downstream stages — #753 (snapshot evidence), #754 (QI source), #811 (nav
6
+ // graph, from `links`), #812 (a11y), #755 (codegen, from `tokens`). All emitted collections are
7
+ // stable-ordered; the structure carries no timestamps so the same input yields a byte-identical IR.
8
+ export {};
@@ -0,0 +1,6 @@
1
+ import { type FigmaSourceNode } from "./sourceNode.js";
2
+ import type { InterScreenLink } from "./irTypes.js";
3
+ import type { PrunedNode } from "./prune.js";
4
+ /** Extract stable-ordered, deduped raw inter-screen links from the pruned tree + raw flow entries. */
5
+ export declare const extractInterScreenLinks: (rawRoot: FigmaSourceNode, screens: readonly PrunedNode[]) => readonly InterScreenLink[];
6
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/links.ts"],"names":[],"mappings":"AAcA,OAAO,EAAyC,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC9F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAqF7C,sGAAsG;AACtG,eAAO,MAAM,uBAAuB,GAClC,SAAS,eAAe,EACxB,SAAS,SAAS,UAAU,EAAE,KAC7B,SAAS,eAAe,EAS1B,CAAC"}
@@ -0,0 +1,102 @@
1
+ // Raw inter-screen link extraction (Epic #750, Issue #752).
2
+ //
3
+ // Captures the raw prototype transitions; the navigation/flow graph itself is derived downstream
4
+ // (#811). Three structural sources, read generically:
5
+ // 1. `interactions[].actions[]` — the CURRENT Figma prototype field. Each action with a
6
+ // destination node id yields `{ sourceNodeId, trigger, targetNodeId }`; trigger comes from the
7
+ // interaction's `trigger.type`.
8
+ // 2. `reactions[].action` — the LEGACY field, tolerated for older payloads, same projection.
9
+ // 3. CANVAS `flowStartingPoints[]` + `prototypeStartNodeID` — flow entry points, emitted as
10
+ // `{ sourceNodeId: <canvasId>, trigger: "FLOW_START", targetNodeId }`.
11
+ //
12
+ // Links come from the pruned tree (hidden/dropped nodes emit nothing) plus the raw root for flow
13
+ // entry points. The output is sorted by a stable structural key so order never depends on traversal.
14
+ import { asNode, nodeId, readArray, readString } from "./sourceNode.js";
15
+ const FLOW_START = "FLOW_START";
16
+ const UNKNOWN_TRIGGER = "UNKNOWN";
17
+ // Shared constant — see prune.ts for rationale. Must stay in sync with every other recursive walk.
18
+ const MAX_TREE_DEPTH = 512;
19
+ const EXTERNAL_LINK_TARGET = /^(?:[a-z][a-z0-9+.-]*:\/\/|mailto:|tel:|data:|javascript:)/iu;
20
+ const isExternalLinkTarget = (targetNodeId) => targetNodeId.startsWith("url:") || EXTERNAL_LINK_TARGET.test(targetNodeId);
21
+ const readDestination = (action) => {
22
+ const direct = readString(action.destinationId);
23
+ if (direct !== undefined)
24
+ return isExternalLinkTarget(direct) ? undefined : direct;
25
+ const nav = asNode(action.navigation);
26
+ if (nav !== undefined) {
27
+ const nested = readString(nav.destinationId);
28
+ return nested !== undefined && !isExternalLinkTarget(nested) ? nested : undefined;
29
+ }
30
+ return undefined;
31
+ };
32
+ const triggerType = (entry) => {
33
+ const trigger = asNode(entry.trigger);
34
+ return (trigger !== undefined ? readString(trigger.type) : undefined) ?? UNKNOWN_TRIGGER;
35
+ };
36
+ const collectFromActions = (sourceNodeId, trigger, actions, out) => {
37
+ for (const action of actions) {
38
+ const record = asNode(action);
39
+ if (record === undefined)
40
+ continue;
41
+ const targetNodeId = readDestination(record);
42
+ if (targetNodeId !== undefined)
43
+ out.push({ sourceNodeId, trigger, targetNodeId });
44
+ }
45
+ };
46
+ const collectInteractions = (node, out) => {
47
+ const sourceNodeId = nodeId(node);
48
+ for (const entry of readArray(node.interactions)) {
49
+ const record = asNode(entry);
50
+ if (record === undefined)
51
+ continue;
52
+ collectFromActions(sourceNodeId, triggerType(record), readArray(record.actions), out);
53
+ }
54
+ for (const entry of readArray(node.reactions)) {
55
+ const record = asNode(entry);
56
+ if (record === undefined)
57
+ continue;
58
+ const action = asNode(record.action);
59
+ if (action === undefined)
60
+ continue;
61
+ collectFromActions(sourceNodeId, triggerType(record), [action], out);
62
+ }
63
+ };
64
+ function visitAt(pruned, out, depth) {
65
+ if (depth > MAX_TREE_DEPTH)
66
+ return;
67
+ collectInteractions(pruned.source, out);
68
+ for (const child of pruned.children)
69
+ visitAt(child, out, depth + 1);
70
+ }
71
+ const visit = (pruned, out) => {
72
+ visitAt(pruned, out, 0);
73
+ };
74
+ const collectFlowEntries = (root, out) => {
75
+ const sourceNodeId = nodeId(root);
76
+ for (const point of readArray(root.flowStartingPoints)) {
77
+ const record = asNode(point);
78
+ const target = record !== undefined ? readString(record.nodeId) : undefined;
79
+ if (target !== undefined)
80
+ out.push({ sourceNodeId, trigger: FLOW_START, targetNodeId: target });
81
+ }
82
+ const start = readString(root.prototypeStartNodeID);
83
+ if (start !== undefined)
84
+ out.push({ sourceNodeId, trigger: FLOW_START, targetNodeId: start });
85
+ };
86
+ // Injective dedup/sort key: JSON.stringify of the field tuple escapes every character (NUL,
87
+ // backslash, quote, ...), so two distinct links can never collide into one key, which would
88
+ // silently drop a transition feeding the downstream navigation graph (#811). Deterministic and
89
+ // stable-ordered, preserving the byte-identical IR contract for every input.
90
+ const linkKey = (link) => JSON.stringify([link.sourceNodeId, link.trigger, link.targetNodeId]);
91
+ /** Extract stable-ordered, deduped raw inter-screen links from the pruned tree + raw flow entries. */
92
+ export const extractInterScreenLinks = (rawRoot, screens) => {
93
+ const collected = [];
94
+ collectFlowEntries(rawRoot, collected);
95
+ for (const screen of screens)
96
+ visit(screen, collected);
97
+ const byKey = new Map();
98
+ for (const link of collected)
99
+ byKey.set(linkKey(link), link);
100
+ const byCode = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
101
+ return [...byKey.values()].sort((a, b) => byCode(linkKey(a), linkKey(b)));
102
+ };
@@ -0,0 +1,74 @@
1
+ import type { ScreenIrResult } from "./irTypes.js";
2
+ import type { StructuralTestItem } from "./screenIrTestBaseline.js";
3
+ /** A graph node: one screen. */
4
+ export interface NavNode {
5
+ readonly screenId: string;
6
+ readonly screenName: string;
7
+ }
8
+ /** A resolved transition between two screens (both endpoints own a screen). */
9
+ export interface NavEdge {
10
+ readonly fromScreenId: string;
11
+ readonly trigger: string;
12
+ readonly toScreenId: string;
13
+ readonly sourceNodeId: string;
14
+ readonly targetNodeId: string;
15
+ }
16
+ /** A raw link whose target node id resolves to no screen — an edge that leaves the known screens. */
17
+ export interface UnresolvedLink {
18
+ readonly sourceNodeId: string;
19
+ readonly trigger: string;
20
+ readonly targetNodeId: string;
21
+ }
22
+ /** The deterministic navigation graph plus reachability analysis. */
23
+ export interface NavGraph {
24
+ readonly nodes: readonly NavNode[];
25
+ readonly edges: readonly NavEdge[];
26
+ readonly unresolvedLinks: readonly UnresolvedLink[];
27
+ /** Screens that are flow entry points (or the synthetic first-screen entry when none exist). */
28
+ readonly entryScreenIds: readonly string[];
29
+ /** Reachable-by-no-edge-or-entry screens, stable-sorted. */
30
+ readonly unreachableScreenIds: readonly string[];
31
+ /** Reachable screens with no outgoing edge, stable-sorted. */
32
+ readonly deadEndScreenIds: readonly string[];
33
+ }
34
+ /** A bounded, acyclic multi-step path through the graph. */
35
+ export interface NavFlow {
36
+ readonly screenIds: readonly string[];
37
+ }
38
+ /** A framework-agnostic routing hint: one screen and its outgoing transitions. */
39
+ export interface RoutingHint {
40
+ readonly screenId: string;
41
+ readonly transitions: readonly {
42
+ readonly trigger: string;
43
+ readonly toScreenId: string;
44
+ }[];
45
+ }
46
+ /**
47
+ * Derive the deterministic navigation graph from a Screen-IR result. When the IR carries no
48
+ * flow-entry link, the first screen in stable id order is used as the synthetic entry so reachability
49
+ * is still well-defined. The result is byte-identical for a given input.
50
+ */
51
+ export declare function deriveNavGraph(ir: ScreenIrResult): NavGraph;
52
+ /**
53
+ * Derive bounded, acyclic multi-step flow paths from the graph's entry screens. `maxDepth` caps the
54
+ * number of screens per path (default {@link DEFAULT_MAX_FLOW_DEPTH}); the per-path visited set makes
55
+ * every path acyclic, so a cycle or self-loop never yields an unbounded path set. Total flows are
56
+ * capped at {@link MAX_NAV_FLOWS} so a dense fully-connected graph cannot exhaust memory. Stable-sorted.
57
+ */
58
+ export declare function deriveNavFlows(graph: NavGraph, maxDepth?: number): readonly NavFlow[];
59
+ /**
60
+ * Framework-agnostic routing hints for design-to-code (#755): every screen mapped to its outgoing
61
+ * transitions (`trigger` + `toScreenId`). No router, path, or framework vocabulary — a neutral
62
+ * structure the target adapter shapes into its own routing. Stable-sorted per screen and transition.
63
+ */
64
+ export declare function deriveRoutingHints(graph: NavGraph): readonly RoutingHint[];
65
+ /**
66
+ * Derive per-screen navigation/flow/coverage test items, keyed by the screen they are attributed to.
67
+ * Reuses #754's StructuralTestItem shape so the items compose additively through
68
+ * `deriveScreenTestBaseline(screen, extraItems)`. Every edge → a navigation item on its source
69
+ * screen; every multi-step flow → a flow item on its entry screen; every unreachable/dead-end screen
70
+ * → a coverage-notice item. When flow enumeration is truncated by MAX_NAV_FLOWS, one additional
71
+ * coverage-notice records the omission. Deterministic: items are emitted in the graph's stable order.
72
+ */
73
+ export declare function deriveNavTestItemsByScreen(graph: NavGraph, maxFlowDepth?: number): ReadonlyMap<string, readonly StructuralTestItem[]>;
74
+ //# sourceMappingURL=navGraph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navGraph.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/navGraph.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAqC,cAAc,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AASpE,gCAAgC;AAChC,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,qGAAqG;AACrG,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,qEAAqE;AACrE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IACpD,gGAAgG;IAChG,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,4DAA4D;IAC5D,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,8DAA8D;IAC9D,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,4DAA4D;AAC5D,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5F;AA6GD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,GAAG,QAAQ,CA0B3D;AA6DD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,EACf,QAAQ,GAAE,MAA+B,GACxC,SAAS,OAAO,EAAE,CAWpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,WAAW,EAAE,CAU1E;AA+ED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,QAAQ,EACf,YAAY,GAAE,MAA+B,GAC5C,WAAW,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC,CAYpD"}
@@ -0,0 +1,315 @@
1
+ // Deterministic screen navigation/flow graph from a Screen-IR (Epic #750, Issue #811).
2
+ //
3
+ // Pure domain: a Screen-IR (#752) carries `screens` (per-screen kept-node trees) and the raw
4
+ // inter-screen `links` (prototype transitions `{ sourceNodeId, trigger, targetNodeId }`). This
5
+ // module reduces them, with NO model and NO IO, to:
6
+ // • a navigation GRAPH — nodes = screens, edges = transitions whose source AND target node ids
7
+ // each resolve to an owning screen; links whose target resolves to no screen are recorded as
8
+ // `unresolvedLinks` (an "edge outside any screen"); flow-entry links (a canvas source outside
9
+ // any screen) mark `entryScreenIds` without an edge;
10
+ // • reachability — `entryScreenIds`, `unreachableScreenIds`, `deadEndScreenIds`;
11
+ // • framework-agnostic ROUTING HINTS (screen → outgoing transitions) for design-to-code (#755),
12
+ // with no router/framework vocabulary;
13
+ // • per-screen navigation/flow/coverage TEST ITEMS reusing #754's StructuralTestItem shape, so
14
+ // they compose into the figma-snapshot QI run additively through `deriveScreenTestBaseline`'s
15
+ // `extraItems` seam.
16
+ //
17
+ // Generic by construction: rules read only structural shape (which node ids own which screen, which
18
+ // trigger reaches which screen). No screen name, layout, mask style, or copy is special-cased.
19
+ // Deterministic: every emitted collection is stable-sorted and carries no timestamp, so the same IR
20
+ // yields a byte-identical result. Flow derivation is bounded (max depth + acyclic visited set) so a
21
+ // cycle or self-loop never produces an unbounded path set. Total flow count is also bounded by
22
+ // MAX_NAV_FLOWS so a dense fully-connected graph cannot exhaust memory during QI ingestion.
23
+ const FLOW_START_TRIGGER = "FLOW_START";
24
+ const DEFAULT_MAX_FLOW_DEPTH = 6;
25
+ // Total cap on enumerated flows: a fully-connected 12-screen graph at depth 6 yields 773,784+ paths
26
+ // and hangs/OOMs QI ingestion. The cap preserves the deterministic prefix (derivation order is
27
+ // already stable) and appends one coverage-notice when truncated. Mirrors MAX_A11Y_ITEMS_PER_SCREEN.
28
+ const MAX_NAV_FLOWS = 500;
29
+ const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
30
+ // Build a nodeId → screenId index by walking every screen's IR tree once. A node id that appears in
31
+ // more than one screen (malformed input) keeps its first (stable-order) owner.
32
+ function buildNodeOwnership(screens) {
33
+ const owner = new Map();
34
+ const visit = (node, screenId) => {
35
+ if (!owner.has(node.id))
36
+ owner.set(node.id, screenId);
37
+ for (const child of node.children)
38
+ visit(child, screenId);
39
+ };
40
+ for (const screen of [...screens].sort((a, b) => byString(a.id, b.id))) {
41
+ visit(screen.root, screen.id);
42
+ }
43
+ return owner;
44
+ }
45
+ // Injective composite key over a field tuple. The previous NUL-delimited join was non-injective:
46
+ // a value carrying the separator byte could shift a field boundary so two distinct tuples collapse
47
+ // to one key — the exact defect #752 fixed in links.ts linkKey, and a Figma node id or trigger from
48
+ // an (untrusted) snapshot is not guaranteed separator-free. JSON.stringify escapes the quote,
49
+ // backslash, and control bytes, so the encoding is total and unambiguous for any input. Used for
50
+ // de-duplication, identity hashing, and stable total ordering so every emitted collection stays
51
+ // byte-identical for a given input.
52
+ const compositeKey = (...parts) => JSON.stringify(parts);
53
+ const edgeKey = (e) => compositeKey(e.fromScreenId, e.trigger, e.toScreenId, e.sourceNodeId, e.targetNodeId);
54
+ const flowKey = (screenIds) => compositeKey(...screenIds);
55
+ // Classify each raw link against the node-ownership index. A target that owns a screen and a source
56
+ // that owns a screen yields an edge; a FLOW_START whose target owns a screen marks an entry; any
57
+ // link whose target owns no screen is unresolved (an edge that leaves the known screens).
58
+ function classifyLinks(links, owner) {
59
+ const edges = new Map();
60
+ const unresolved = [];
61
+ const entries = new Set();
62
+ for (const link of links) {
63
+ const toScreenId = owner.get(link.targetNodeId);
64
+ if (toScreenId === undefined) {
65
+ unresolved.push({ ...link });
66
+ continue;
67
+ }
68
+ const fromScreenId = owner.get(link.sourceNodeId);
69
+ if (fromScreenId === undefined) {
70
+ if (link.trigger === FLOW_START_TRIGGER)
71
+ entries.add(toScreenId);
72
+ continue;
73
+ }
74
+ const edge = {
75
+ fromScreenId,
76
+ trigger: link.trigger,
77
+ toScreenId,
78
+ sourceNodeId: link.sourceNodeId,
79
+ targetNodeId: link.targetNodeId,
80
+ };
81
+ edges.set(edgeKey(edge), edge);
82
+ }
83
+ return {
84
+ edges: [...edges.values()].sort((a, b) => byString(edgeKey(a), edgeKey(b))),
85
+ // Total order: targetNodeId alone is not injective across unresolved links sharing a target, so
86
+ // input order would leak into the output and break the byte-identical guarantee (DET1). The
87
+ // full tuple key restores a deterministic total order under ties.
88
+ unresolved: [...unresolved].sort((a, b) => byString(compositeKey(a.targetNodeId, a.trigger, a.sourceNodeId), compositeKey(b.targetNodeId, b.trigger, b.sourceNodeId))),
89
+ entryScreenIds: [...entries].sort(byString),
90
+ };
91
+ }
92
+ // Reachable set by BFS from the entry screens over the directed edges.
93
+ function reachableFrom(entryScreenIds, edges) {
94
+ const adjacency = new Map();
95
+ for (const edge of edges) {
96
+ const list = adjacency.get(edge.fromScreenId) ?? [];
97
+ list.push(edge.toScreenId);
98
+ adjacency.set(edge.fromScreenId, list);
99
+ }
100
+ const reached = new Set(entryScreenIds);
101
+ const queue = [...entryScreenIds];
102
+ while (queue.length > 0) {
103
+ const current = queue.shift();
104
+ if (current === undefined)
105
+ continue;
106
+ for (const next of adjacency.get(current) ?? []) {
107
+ if (!reached.has(next)) {
108
+ reached.add(next);
109
+ queue.push(next);
110
+ }
111
+ }
112
+ }
113
+ return reached;
114
+ }
115
+ /**
116
+ * Derive the deterministic navigation graph from a Screen-IR result. When the IR carries no
117
+ * flow-entry link, the first screen in stable id order is used as the synthetic entry so reachability
118
+ * is still well-defined. The result is byte-identical for a given input.
119
+ */
120
+ export function deriveNavGraph(ir) {
121
+ const nodes = [...ir.screens]
122
+ .map((s) => ({ screenId: s.id, screenName: s.name }))
123
+ .sort((a, b) => byString(a.screenId, b.screenId));
124
+ const owner = buildNodeOwnership(ir.screens);
125
+ const { edges, unresolved, entryScreenIds } = classifyLinks(ir.links, owner);
126
+ const effectiveEntries = entryScreenIds.length > 0
127
+ ? entryScreenIds
128
+ : nodes.length > 0 && nodes[0] !== undefined
129
+ ? [nodes[0].screenId]
130
+ : [];
131
+ const reached = reachableFrom(effectiveEntries, edges);
132
+ const withOutgoing = new Set(edges.map((e) => e.fromScreenId));
133
+ return {
134
+ nodes,
135
+ edges,
136
+ unresolvedLinks: unresolved,
137
+ entryScreenIds: effectiveEntries,
138
+ unreachableScreenIds: nodes.map((n) => n.screenId).filter((id) => !reached.has(id)),
139
+ deadEndScreenIds: nodes
140
+ .map((n) => n.screenId)
141
+ .filter((id) => reached.has(id) && !withOutgoing.has(id)),
142
+ };
143
+ }
144
+ // Depth-first enumeration of acyclic paths from one entry, bounded by maxDepth, the visited set, and
145
+ // the shared counter so dense graphs cannot exhaust memory or hang QI ingestion. Each path of length
146
+ // ≥ 2 is a flow; the single-screen entry path is not emitted. Paths are in deterministic edge order.
147
+ function walkFlows(start, adjacency, maxDepth, cap, counter, out) {
148
+ const recurse = (path) => {
149
+ if (counter.count >= cap)
150
+ return;
151
+ if (path.length >= 2) {
152
+ out.push([...path]);
153
+ counter.count += 1;
154
+ }
155
+ if (path.length >= maxDepth)
156
+ return;
157
+ const last = path[path.length - 1];
158
+ if (last === undefined)
159
+ return;
160
+ for (const next of adjacency.get(last) ?? []) {
161
+ if (counter.count >= cap)
162
+ return;
163
+ if (!path.includes(next))
164
+ recurse([...path, next]);
165
+ }
166
+ };
167
+ recurse([start]);
168
+ }
169
+ // Build the adjacency map once; edges are already stable-sorted so adjacency lists are deterministic.
170
+ // Parallel edges (distinct prototype reactions from the same screen to the same target — two buttons,
171
+ // or the same button with two triggers, both reaching A → B) must NOT push duplicate target entries:
172
+ // walkFlows would traverse the identical sub-path once per duplicate, inflating the shared flow
173
+ // counter past the number of DISTINCT flows. Because deriveNavTestItemsByScreen decides whether to
174
+ // emit the truncation notice from the post-dedup flows.length, an inflated counter can hit
175
+ // MAX_NAV_FLOWS and truncate enumeration while flows.length stays below the cap — silently dropping
176
+ // flows with NO coverage-notice (CORR1). Navigation adjacency is a reachability relation, so a
177
+ // per-source target set is the correct model and keeps the counter equal to the distinct-flow count.
178
+ // First-occurrence order is preserved (edges are stable-sorted) so the derivation stays byte-identical.
179
+ function buildAdjacency(edges) {
180
+ const adj = new Map();
181
+ const seen = new Map();
182
+ for (const edge of edges) {
183
+ const list = adj.get(edge.fromScreenId) ?? [];
184
+ const seenTargets = seen.get(edge.fromScreenId) ?? new Set();
185
+ if (seenTargets.has(edge.toScreenId))
186
+ continue;
187
+ list.push(edge.toScreenId);
188
+ seenTargets.add(edge.toScreenId);
189
+ adj.set(edge.fromScreenId, list);
190
+ seen.set(edge.fromScreenId, seenTargets);
191
+ }
192
+ return adj;
193
+ }
194
+ /**
195
+ * Derive bounded, acyclic multi-step flow paths from the graph's entry screens. `maxDepth` caps the
196
+ * number of screens per path (default {@link DEFAULT_MAX_FLOW_DEPTH}); the per-path visited set makes
197
+ * every path acyclic, so a cycle or self-loop never yields an unbounded path set. Total flows are
198
+ * capped at {@link MAX_NAV_FLOWS} so a dense fully-connected graph cannot exhaust memory. Stable-sorted.
199
+ */
200
+ export function deriveNavFlows(graph, maxDepth = DEFAULT_MAX_FLOW_DEPTH) {
201
+ const adjacency = buildAdjacency(graph.edges);
202
+ const collected = [];
203
+ const counter = { count: 0 };
204
+ for (const entry of graph.entryScreenIds) {
205
+ if (counter.count >= MAX_NAV_FLOWS)
206
+ break;
207
+ walkFlows(entry, adjacency, maxDepth, MAX_NAV_FLOWS, counter, collected);
208
+ }
209
+ const byKey = new Map();
210
+ for (const screenIds of collected)
211
+ byKey.set(flowKey(screenIds), { screenIds });
212
+ return [...byKey.values()].sort((a, b) => byString(flowKey(a.screenIds), flowKey(b.screenIds)));
213
+ }
214
+ /**
215
+ * Framework-agnostic routing hints for design-to-code (#755): every screen mapped to its outgoing
216
+ * transitions (`trigger` + `toScreenId`). No router, path, or framework vocabulary — a neutral
217
+ * structure the target adapter shapes into its own routing. Stable-sorted per screen and transition.
218
+ */
219
+ export function deriveRoutingHints(graph) {
220
+ return graph.nodes.map((node) => ({
221
+ screenId: node.screenId,
222
+ transitions: graph.edges
223
+ .filter((e) => e.fromScreenId === node.screenId)
224
+ .map((e) => ({ trigger: e.trigger, toScreenId: e.toScreenId }))
225
+ .sort((a, b) => byString(compositeKey(a.trigger, a.toScreenId), compositeKey(b.trigger, b.toScreenId))),
226
+ }));
227
+ }
228
+ const screenNameOf = (graph, screenId) => graph.nodes.find((n) => n.screenId === screenId)?.screenName ?? screenId;
229
+ function navItemId(prefix, key) {
230
+ // Deterministic non-cryptographic id (FNV-1a) — stable across runs, no IO. Mirrors #754's scheme.
231
+ let hash = 0x811c9dc5;
232
+ for (let i = 0; i < key.length; i += 1) {
233
+ hash ^= key.charCodeAt(i);
234
+ hash = Math.imul(hash, 0x01000193);
235
+ }
236
+ return `${prefix}-${(hash >>> 0).toString(16).padStart(8, "0")}`;
237
+ }
238
+ function navigationItem(graph, edge) {
239
+ const from = screenNameOf(graph, edge.fromScreenId);
240
+ const to = screenNameOf(graph, edge.toScreenId);
241
+ return {
242
+ id: navItemId("fnav", edgeKey(edge)),
243
+ category: "navigation",
244
+ screenId: edge.fromScreenId,
245
+ screenName: from,
246
+ sourceNodeId: edge.sourceNodeId,
247
+ title: `From screen "${from}", trigger "${edge.trigger}" reaches screen "${to}"`,
248
+ };
249
+ }
250
+ function flowItem(graph, flow) {
251
+ const entry = flow.screenIds[0] ?? "";
252
+ const names = flow.screenIds.map((id) => `"${screenNameOf(graph, id)}"`).join(" → ");
253
+ return {
254
+ id: navItemId("fflow", flowKey(flow.screenIds)),
255
+ category: "flow",
256
+ screenId: entry,
257
+ screenName: screenNameOf(graph, entry),
258
+ title: `Flow ${names} completes end to end`,
259
+ };
260
+ }
261
+ function coverageItem(graph, screenId, kind) {
262
+ const name = screenNameOf(graph, screenId);
263
+ const title = kind === "unreachable"
264
+ ? `Screen "${name}" is unreachable: no navigation reaches it`
265
+ : `Screen "${name}" is a dead end: no navigation leaves it`;
266
+ return {
267
+ id: navItemId("fcov", compositeKey(kind, screenId)),
268
+ category: "coverage-notice",
269
+ screenId,
270
+ screenName: name,
271
+ title,
272
+ };
273
+ }
274
+ // Emitted as a coverage-notice when flow enumeration was truncated by MAX_NAV_FLOWS. Attributed to
275
+ // the synthetic entry screen (stable: first entry id) so it appears in a well-defined byScreen slot.
276
+ function flowCapNoticeItem(graph, totalEnumerated) {
277
+ const entryId = graph.entryScreenIds[0] ?? graph.nodes[0]?.screenId ?? "";
278
+ const name = screenNameOf(graph, entryId);
279
+ return {
280
+ id: navItemId("fnavcap", compositeKey("cap", entryId)),
281
+ category: "coverage-notice",
282
+ screenId: entryId,
283
+ screenName: name,
284
+ title: `Flow enumeration was truncated at ${String(MAX_NAV_FLOWS)} paths (${String(totalEnumerated)} emitted); denser graphs may have additional flows`,
285
+ };
286
+ }
287
+ function pushItem(byScreen, item) {
288
+ const list = byScreen.get(item.screenId) ?? [];
289
+ list.push(item);
290
+ byScreen.set(item.screenId, list);
291
+ }
292
+ /**
293
+ * Derive per-screen navigation/flow/coverage test items, keyed by the screen they are attributed to.
294
+ * Reuses #754's StructuralTestItem shape so the items compose additively through
295
+ * `deriveScreenTestBaseline(screen, extraItems)`. Every edge → a navigation item on its source
296
+ * screen; every multi-step flow → a flow item on its entry screen; every unreachable/dead-end screen
297
+ * → a coverage-notice item. When flow enumeration is truncated by MAX_NAV_FLOWS, one additional
298
+ * coverage-notice records the omission. Deterministic: items are emitted in the graph's stable order.
299
+ */
300
+ export function deriveNavTestItemsByScreen(graph, maxFlowDepth = DEFAULT_MAX_FLOW_DEPTH) {
301
+ const byScreen = new Map();
302
+ for (const edge of graph.edges)
303
+ pushItem(byScreen, navigationItem(graph, edge));
304
+ const flows = deriveNavFlows(graph, maxFlowDepth);
305
+ for (const flow of flows)
306
+ pushItem(byScreen, flowItem(graph, flow));
307
+ if (flows.length >= MAX_NAV_FLOWS) {
308
+ pushItem(byScreen, flowCapNoticeItem(graph, flows.length));
309
+ }
310
+ for (const id of graph.unreachableScreenIds)
311
+ pushItem(byScreen, coverageItem(graph, id, "unreachable"));
312
+ for (const id of graph.deadEndScreenIds)
313
+ pushItem(byScreen, coverageItem(graph, id, "dead end"));
314
+ return byScreen;
315
+ }
@@ -0,0 +1,7 @@
1
+ import type { IrNode } from "./irTypes.js";
2
+ import type { PrunedNode } from "./prune.js";
3
+ /** Normalize a pruned screen root into its IR node tree. Child order follows source order. */
4
+ export declare const normalizeScreenRoot: (pruned: PrunedNode) => IrNode;
5
+ /** Count the IR nodes in a normalized tree, used for the reduction ratio. */
6
+ export declare const countIrNodes: (node: IrNode) => number;
7
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/normalize.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAMV,MAAM,EAKP,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAiO7C,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,GAAI,QAAQ,UAAU,KAAG,MAAgC,CAAC;AAO1F,6EAA6E;AAC7E,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAAiC,CAAC"}