@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,252 @@
1
+ // Per-node normalization of the pruned tree into IR nodes (Epic #750, Issue #752).
2
+ //
3
+ // Projects each kept node to a compact `IrNode`: id/name/type, text content, bounding box, image
4
+ // fill refs, kept children, and a best-effort `interactionHint`.
5
+ //
6
+ // In addition to the structural fields, five additive optional fields are projected when present:
7
+ // layout — auto-layout direction/gap/padding/alignment (HORIZONTAL/VERTICAL layoutMode)
8
+ // sizing — per-axis sizing mode (layoutSizingHorizontal/Vertical: FIXED|HUG|FILL)
9
+ // cornerRadius — corner radius in pixels (cornerRadius > 0)
10
+ // typography — per-TEXT fontFamily/fontSize/fontWeight (from the `style` block)
11
+ //
12
+ // These fields are OMITTED when absent or when the source values are missing/malformed — never
13
+ // defaulted. They are hash-neutral (#753/#735): re-normalizing the same structural design after
14
+ // these fields ship does not change the drift identity. Negative, NaN, and non-finite values are
15
+ // clamped to absent.
16
+ //
17
+ // The interaction hint is a HINT, never load-bearing — downstream (#754) treats button/input
18
+ // classification as advisory and degrades to `container`. Three classes are purely structural:
19
+ // link — the node carries a navigating prototype interaction/reaction (it navigates).
20
+ // image — the node has an IMAGE-type fill (and is not TEXT).
21
+ // text — the node is a TEXT node.
22
+ // `button` is also used for activation-style local prototype actions (for example ON_CLICK variable
23
+ // updates) because they are interactive controls even when they do not navigate. `button`/`input`
24
+ // name heuristics remain a tiny, word-boundary, case-insensitive match over conventional
25
+ // design-system role vocabulary. Boards that don't use these words and carry no prototype action
26
+ // fall back to `container` — no board's specific names are encoded.
27
+ import { asNode, nodeId, nodeName, nodeType, readArray, readNumber, readString, } from "./sourceNode.js";
28
+ import { firstSolidPaintHex, isVisiblePaint } from "./color.js";
29
+ const BUTTON_ROLE = /\b(?:button|btn|cta)\b/iu;
30
+ const INPUT_ROLE = /\b(?:input|field|textfield|textbox)\b/iu;
31
+ const readBoundingBox = (node) => {
32
+ const box = asNode(node.absoluteBoundingBox);
33
+ if (box === undefined)
34
+ return undefined;
35
+ const x = readNumber(box.x);
36
+ const y = readNumber(box.y);
37
+ const width = readNumber(box.width);
38
+ const height = readNumber(box.height);
39
+ if (x === undefined || y === undefined || width === undefined || height === undefined) {
40
+ return undefined;
41
+ }
42
+ return { x, y, width, height };
43
+ };
44
+ const readImageFills = (node) => {
45
+ const out = [];
46
+ for (const fill of readArray(node.fills)) {
47
+ const record = asNode(fill);
48
+ if (record === undefined || readString(record.type) !== "IMAGE")
49
+ continue;
50
+ if (!isVisiblePaint(record))
51
+ continue;
52
+ const imageRef = readString(record.imageRef);
53
+ if (imageRef !== undefined)
54
+ out.push({ imageRef });
55
+ }
56
+ return out;
57
+ };
58
+ const actionHasNavigationTarget = (action) => {
59
+ const record = asNode(action);
60
+ if (record === undefined)
61
+ return false;
62
+ if (readString(record.destinationId) !== undefined)
63
+ return true;
64
+ const nav = asNode(record.navigation);
65
+ if (nav !== undefined && readString(nav.destinationId) !== undefined)
66
+ return true;
67
+ return readString(record.type) === "URL" && readString(record.url) !== undefined;
68
+ };
69
+ const interactionHasNavigationTarget = (entry) => {
70
+ const record = asNode(entry);
71
+ if (record === undefined)
72
+ return false;
73
+ if (readArray(record.actions).some(actionHasNavigationTarget))
74
+ return true;
75
+ return actionHasNavigationTarget(record.action);
76
+ };
77
+ const interactionHasLocalAction = (entry) => {
78
+ const record = asNode(entry);
79
+ if (record === undefined)
80
+ return false;
81
+ const trigger = asNode(record.trigger);
82
+ const triggerType = trigger !== undefined ? readString(trigger.type) : undefined;
83
+ if (triggerType !== "ON_CLICK" && triggerType !== "ON_PRESS" && triggerType !== "ON_TAP") {
84
+ return false;
85
+ }
86
+ return readArray(record.actions).length > 0 || asNode(record.action) !== undefined;
87
+ };
88
+ const navigates = (node) => {
89
+ const interactions = readArray(node.interactions);
90
+ const reactions = readArray(node.reactions);
91
+ return (interactions.some(interactionHasNavigationTarget) ||
92
+ reactions.some(interactionHasNavigationTarget));
93
+ };
94
+ const hasActivation = (node) => {
95
+ const interactions = readArray(node.interactions);
96
+ const reactions = readArray(node.reactions);
97
+ return interactions.some(interactionHasLocalAction) || reactions.some(interactionHasLocalAction);
98
+ };
99
+ const classify = (node, imageFills) => {
100
+ if (nodeType(node) === "TEXT")
101
+ return "text";
102
+ if (navigates(node))
103
+ return "link";
104
+ if (hasActivation(node))
105
+ return "button";
106
+ if (imageFills.length > 0)
107
+ return "image";
108
+ const name = nodeName(node);
109
+ if (BUTTON_ROLE.test(name))
110
+ return "button";
111
+ if (INPUT_ROLE.test(name))
112
+ return "input";
113
+ return "container";
114
+ };
115
+ // A TEXT node's solid fill is its foreground (text) colour; any other node's solid fill is a
116
+ // background. We project at most one of each so the a11y contrast pass (#812) has a deterministic
117
+ // text-vs-background pairing without re-deriving paints. Both are absent when there is no solid fill.
118
+ const readTextColor = (node) => nodeType(node) === "TEXT" ? firstSolidPaintHex(node, "fills") : undefined;
119
+ const readBackgroundColor = (node) => nodeType(node) === "TEXT" ? undefined : firstSolidPaintHex(node, "fills");
120
+ // ─── Layout / sizing / cornerRadius / typography projection ──────────────────
121
+ const ALIGN_MAP = {
122
+ MIN: "start",
123
+ CENTER: "center",
124
+ MAX: "end",
125
+ SPACE_BETWEEN: "space-between",
126
+ };
127
+ const readAlign = (value) => {
128
+ const s = readString(value);
129
+ return s !== undefined ? ALIGN_MAP[s] : undefined;
130
+ };
131
+ const readLayoutMode = (value) => {
132
+ const s = readString(value);
133
+ if (s === "HORIZONTAL")
134
+ return "row";
135
+ if (s === "VERTICAL")
136
+ return "column";
137
+ return undefined;
138
+ };
139
+ const readLayoutSizing = (value) => {
140
+ const s = readString(value);
141
+ if (s === "FIXED")
142
+ return "fixed";
143
+ if (s === "HUG")
144
+ return "hug";
145
+ if (s === "FILL")
146
+ return "fill";
147
+ return undefined;
148
+ };
149
+ // A padding value is valid only when it is a finite, non-negative number.
150
+ const readPaddingValue = (value) => {
151
+ const n = readNumber(value);
152
+ return n !== undefined && n >= 0 ? n : undefined;
153
+ };
154
+ // Returns [top, right, bottom, left] when at least one side is non-zero; undefined otherwise.
155
+ const readLayoutPadding = (node) => {
156
+ const top = readPaddingValue(node.paddingTop) ?? 0;
157
+ const right = readPaddingValue(node.paddingRight) ?? 0;
158
+ const bottom = readPaddingValue(node.paddingBottom) ?? 0;
159
+ const left = readPaddingValue(node.paddingLeft) ?? 0;
160
+ return top > 0 || right > 0 || bottom > 0 || left > 0 ? [top, right, bottom, left] : undefined;
161
+ };
162
+ const readLayout = (node) => {
163
+ const mode = readLayoutMode(node.layoutMode);
164
+ if (mode === undefined)
165
+ return undefined;
166
+ const itemSpacing = readNumber(node.itemSpacing);
167
+ const padding = readLayoutPadding(node);
168
+ const primaryAlign = readAlign(node.primaryAxisAlignItems);
169
+ const counterAlign = readAlign(node.counterAxisAlignItems);
170
+ return {
171
+ mode,
172
+ ...(itemSpacing !== undefined && itemSpacing > 0 ? { itemSpacing } : {}),
173
+ ...(padding !== undefined ? { padding } : {}),
174
+ ...(primaryAlign !== undefined ? { primaryAlign } : {}),
175
+ ...(counterAlign !== undefined ? { counterAlign } : {}),
176
+ };
177
+ };
178
+ const readSizing = (node) => {
179
+ const horizontal = readLayoutSizing(node.layoutSizingHorizontal);
180
+ const vertical = readLayoutSizing(node.layoutSizingVertical);
181
+ if (horizontal === undefined && vertical === undefined)
182
+ return undefined;
183
+ return {
184
+ ...(horizontal !== undefined ? { horizontal } : {}),
185
+ ...(vertical !== undefined ? { vertical } : {}),
186
+ };
187
+ };
188
+ const readCornerRadius = (node) => {
189
+ const value = readNumber(node.cornerRadius);
190
+ return value !== undefined && value > 0 ? value : undefined;
191
+ };
192
+ const readTypography = (node) => {
193
+ if (nodeType(node) !== "TEXT")
194
+ return undefined;
195
+ const style = asNode(node.style);
196
+ if (style === undefined)
197
+ return undefined;
198
+ const fontFamily = readString(style.fontFamily);
199
+ const fontSize = readNumber(style.fontSize);
200
+ const fontWeight = readNumber(style.fontWeight);
201
+ const lineHeight = readNumber(style.lineHeightPx);
202
+ if (fontFamily === undefined || fontSize === undefined || fontWeight === undefined)
203
+ return undefined;
204
+ return {
205
+ fontFamily,
206
+ fontSize,
207
+ fontWeight,
208
+ ...(lineHeight !== undefined ? { lineHeight } : {}),
209
+ };
210
+ };
211
+ // Subtrees deeper than this are truncated to prevent RangeError on malformed chain-like inputs.
212
+ // Same shared constant as prune.ts — see there for the rationale. Must stay in sync with every
213
+ // other recursive walk in this pipeline (tokens, links, a11y, screenIrTestBaseline).
214
+ const MAX_TREE_DEPTH = 512;
215
+ function buildNodeAt(pruned, depth) {
216
+ const node = pruned.source;
217
+ const imageFills = readImageFills(node);
218
+ const text = readString(node.characters);
219
+ const boundingBox = readBoundingBox(node);
220
+ const textColor = readTextColor(node);
221
+ const backgroundColor = readBackgroundColor(node);
222
+ const children = depth >= MAX_TREE_DEPTH ? [] : pruned.children.map((c) => buildNodeAt(c, depth + 1));
223
+ const layout = readLayout(node);
224
+ const sizing = readSizing(node);
225
+ const cornerRadius = readCornerRadius(node);
226
+ const typography = readTypography(node);
227
+ return {
228
+ id: nodeId(node),
229
+ name: nodeName(node),
230
+ type: nodeType(node),
231
+ interactionHint: classify(node, imageFills),
232
+ ...(text !== undefined ? { text } : {}),
233
+ ...(boundingBox !== undefined ? { boundingBox } : {}),
234
+ ...(textColor !== undefined ? { textColor } : {}),
235
+ ...(backgroundColor !== undefined ? { backgroundColor } : {}),
236
+ ...(layout !== undefined ? { layout } : {}),
237
+ ...(sizing !== undefined ? { sizing } : {}),
238
+ ...(cornerRadius !== undefined ? { cornerRadius } : {}),
239
+ ...(typography !== undefined ? { typography } : {}),
240
+ imageFills,
241
+ children,
242
+ };
243
+ }
244
+ /** Normalize a pruned screen root into its IR node tree. Child order follows source order. */
245
+ export const normalizeScreenRoot = (pruned) => buildNodeAt(pruned, 0);
246
+ function countIrNodesAt(node, depth) {
247
+ if (depth > MAX_TREE_DEPTH)
248
+ return 1;
249
+ return 1 + node.children.reduce((sum, child) => sum + countIrNodesAt(child, depth + 1), 0);
250
+ }
251
+ /** Count the IR nodes in a normalized tree, used for the reduction ratio. */
252
+ export const countIrNodes = (node) => countIrNodesAt(node, 0);
@@ -0,0 +1,15 @@
1
+ import { type FigmaSourceNode } from "./sourceNode.js";
2
+ /** A node that survived pruning, paired with its pruned children. */
3
+ export interface PrunedNode {
4
+ readonly source: FigmaSourceNode;
5
+ readonly children: readonly PrunedNode[];
6
+ }
7
+ /**
8
+ * Prune a node, returning the kept node (with kept children) or `undefined` when the node itself is
9
+ * dropped. A node is dropped when hidden, a component master, or an empty scaffold. Subtrees deeper
10
+ * than MAX_TREE_DEPTH are truncated so malformed chain-like inputs degrade rather than overflow.
11
+ */
12
+ export declare const pruneNode: (node: FigmaSourceNode) => PrunedNode | undefined;
13
+ /** Total node count of a raw subtree (root included), used for the reduction ratio. */
14
+ export declare const countSourceNodes: (node: FigmaSourceNode) => number;
15
+ //# sourceMappingURL=prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/prune.ts"],"names":[],"mappings":"AAaA,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,iBAAiB,CAAC;AAazB,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AA0BD;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,eAAe,KAAG,UAAU,GAAG,SAAiC,CAAC;AASjG,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,GAAI,MAAM,eAAe,KAAG,MAAqC,CAAC"}
@@ -0,0 +1,65 @@
1
+ // Deterministic pruning of the raw Figma node subtree (Epic #750, Issue #752).
2
+ //
3
+ // Three structural drop rules, justified without any board-specific tuning:
4
+ // 1. `visible === false` — the node is explicitly hidden in Figma; the whole subtree is dropped.
5
+ // 2. type COMPONENT / COMPONENT_SET — design-system definition masters, render-irrelevant. (They
6
+ // may be absent in an instance-heavy scoped fetch; dropped only when present.) INSTANCE is the
7
+ // placed, rendered UI and is always kept.
8
+ // 3. empty scaffold — a node with NO render payload (no text, no fills/strokes, no bbox) AND no
9
+ // kept descendants is a structural marker/wrapper carrying nothing; it is dropped. Anything
10
+ // with payload or surviving children is kept, so the rule never deletes real UI.
11
+ //
12
+ // Pruning is name-agnostic. No copy, screen name, or component vocabulary participates.
13
+ import { asNode, childNodes, isHidden, nodeType, readArray, readString, } from "./sourceNode.js";
14
+ const MASTER_TYPES = new Set(["COMPONENT", "COMPONENT_SET"]);
15
+ // Subtrees deeper than this are truncated (not walked). Prevents RangeError on malformed inputs with
16
+ // chain-like node trees thousands of levels deep. Documented contract: malformed input degrades, never
17
+ // crashes (cleanToScreenIr.ts header).
18
+ // Shared contract: every recursive tree walk in this pipeline (prune → normalize → tokens → links →
19
+ // a11y → screenIrTestBaseline) uses the same value so that none overflow before the others. 512 is
20
+ // far above any legitimate Figma board depth (< 50 in practice) while staying safe inside vitest
21
+ // worker threads, which have a smaller default JS stack than bare Node.
22
+ const MAX_TREE_DEPTH = 512;
23
+ const hasOwnPayload = (node) => {
24
+ if (readString(node.characters) !== undefined)
25
+ return true;
26
+ if (readArray(node.fills).length > 0)
27
+ return true;
28
+ if (readArray(node.strokes).length > 0)
29
+ return true;
30
+ if (asNode(node.absoluteBoundingBox) !== undefined)
31
+ return true;
32
+ return false;
33
+ };
34
+ const isDroppedByType = (node) => MASTER_TYPES.has(nodeType(node));
35
+ function pruneNodeAt(node, depth) {
36
+ if (depth > MAX_TREE_DEPTH)
37
+ return undefined;
38
+ if (isHidden(node) || isDroppedByType(node))
39
+ return undefined;
40
+ const children = [];
41
+ for (const child of childNodes(node)) {
42
+ const kept = pruneNodeAt(child, depth + 1);
43
+ if (kept !== undefined)
44
+ children.push(kept);
45
+ }
46
+ if (children.length === 0 && !hasOwnPayload(node))
47
+ return undefined;
48
+ return { source: node, children };
49
+ }
50
+ /**
51
+ * Prune a node, returning the kept node (with kept children) or `undefined` when the node itself is
52
+ * dropped. A node is dropped when hidden, a component master, or an empty scaffold. Subtrees deeper
53
+ * than MAX_TREE_DEPTH are truncated so malformed chain-like inputs degrade rather than overflow.
54
+ */
55
+ export const pruneNode = (node) => pruneNodeAt(node, 0);
56
+ function countSourceNodesAt(node, depth) {
57
+ if (depth > MAX_TREE_DEPTH)
58
+ return 1; // count the truncated subtree root only
59
+ let total = 1;
60
+ for (const child of childNodes(node))
61
+ total += countSourceNodesAt(child, depth + 1);
62
+ return total;
63
+ }
64
+ /** Total node count of a raw subtree (root included), used for the reduction ratio. */
65
+ export const countSourceNodes = (node) => countSourceNodesAt(node, 0);
@@ -0,0 +1,8 @@
1
+ import type { PrunedNode } from "./prune.js";
2
+ /**
3
+ * Collect the screen roots from a pruned tree. Descends through container nodes (CANVAS/SECTION)
4
+ * collecting their screen children; does not recurse into screens. A directly-scoped screen root is
5
+ * returned as the sole screen.
6
+ */
7
+ export declare const detectScreens: (root: PrunedNode) => readonly PrunedNode[];
8
+ //# sourceMappingURL=screenDetect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenDetect.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/screenDetect.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAS7C;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,UAAU,KAAG,SAAS,UAAU,EAenE,CAAC"}
@@ -0,0 +1,35 @@
1
+ // Structural screen detection over the pruned Figma subtree (Epic #750, Issue #752).
2
+ //
3
+ // A "screen" is a top-level layout container: a FRAME or INSTANCE that sits directly under the
4
+ // scoped root or under a CANVAS/SECTION container directly beneath it. We descend ONLY through the
5
+ // container types (CANVAS, SECTION) that group screens, never into a screen's own body — so nested
6
+ // frames inside a screen stay part of that screen, not separate screens. If the root itself is a
7
+ // FRAME/INSTANCE (a directly-scoped single screen), it is the one screen. Detection is name-agnostic.
8
+ import { nodeType } from "./sourceNode.js";
9
+ const SCREEN_TYPES = new Set(["FRAME", "INSTANCE"]);
10
+ const CONTAINER_TYPES = new Set(["CANVAS", "SECTION"]);
11
+ const isScreen = (node) => SCREEN_TYPES.has(nodeType(node));
12
+ const isContainer = (node) => CONTAINER_TYPES.has(nodeType(node));
13
+ /**
14
+ * Collect the screen roots from a pruned tree. Descends through container nodes (CANVAS/SECTION)
15
+ * collecting their screen children; does not recurse into screens. A directly-scoped screen root is
16
+ * returned as the sole screen.
17
+ */
18
+ export const detectScreens = (root) => {
19
+ if (isScreen(root.source))
20
+ return [root];
21
+ if (!isContainer(root.source))
22
+ return [];
23
+ const screens = [];
24
+ for (const child of root.children) {
25
+ if (isScreen(child.source)) {
26
+ screens.push(child);
27
+ continue;
28
+ }
29
+ if (isContainer(child.source)) {
30
+ for (const nested of detectScreens(child))
31
+ screens.push(nested);
32
+ }
33
+ }
34
+ return screens;
35
+ };
@@ -0,0 +1,52 @@
1
+ import type { ScreenIr } from "./irTypes.js";
2
+ export type StructuralTestCategory = "field-presence" | "field-validation" | "control-action" | "screen-render" | "state" | "navigation" | "flow" | "a11y" | "coverage-notice";
3
+ export type StructuralTestOutcome = "pass" | "fail" | "expectation";
4
+ /** One deterministic, per-screen-attributable test item derived from the Screen-IR. */
5
+ export interface StructuralTestItem {
6
+ readonly id: string;
7
+ readonly title: string;
8
+ readonly category: StructuralTestCategory;
9
+ /** Optional machine-readable result semantics for derived checks. */
10
+ readonly outcome?: StructuralTestOutcome;
11
+ /** Screen provenance so a generated test is attributable to its origin screen. */
12
+ readonly screenId: string;
13
+ readonly screenName: string;
14
+ /** The IR node the item was derived from, when applicable (screen-level items omit it). */
15
+ readonly sourceNodeId?: string;
16
+ }
17
+ /** The structural baseline for a single screen: ordered, deterministic test items. */
18
+ export interface ScreenTestBaseline {
19
+ readonly screenId: string;
20
+ readonly screenName: string;
21
+ readonly items: readonly StructuralTestItem[];
22
+ }
23
+ export declare const MAX_IR_NODES_PER_SCREEN = 20000;
24
+ /**
25
+ * Total, defensive Screen-IR parser for the snapshot's opaque `irJson`. Returns `undefined` for a
26
+ * missing or malformed value (no `root`, malformed node tree) so the caller can skip an unparseable
27
+ * screen without crashing. A valid IR is returned verbatim through the typed shape.
28
+ */
29
+ export declare function parseScreenIr(value: unknown): ScreenIr | undefined;
30
+ /**
31
+ * Derive the deterministic structural baseline for one screen. The screen-render item comes first,
32
+ * then field / control / state items in stable depth-first node order. `extraItems` is the additive
33
+ * seam (#811 navigation, #812 a11y): a sibling derivation may contribute already-built test items
34
+ * for this screen without changing the baseline shape. The result is reproducible for a given IR.
35
+ */
36
+ export declare function deriveScreenTestBaseline(screen: ScreenIr, extraItems?: readonly StructuralTestItem[]): ScreenTestBaseline;
37
+ /**
38
+ * Render the structural baseline as citation-ready canonical text carrying per-screen provenance.
39
+ * The text is deterministic (no timestamps, stable order) and is what the QI ingestion turns into a
40
+ * content-bearing atom. Vision-derived hints, when present, are appended SEPARATELY by the caller
41
+ * (visionAugmentation.ts) and never replace these lines.
42
+ */
43
+ /**
44
+ * Stable marker line introducing the structural baseline inside a Figma screen atom's canonical text.
45
+ * Exported so the deterministic test-design model (`testDesignModel.ts`) can recognise a Figma screen
46
+ * atom and emit a clean, screen-scoped structural candidate instead of the generic prose-requirement
47
+ * template (which produces an atom-id/hash-laden stub for a structural baseline). Intentionally NOT
48
+ * re-exported from the figma barrel (`domain/figma/index.ts`), so it stays off the package surface.
49
+ */
50
+ export declare const STRUCTURAL_BASELINE_MARKER = "Structural test baseline (deterministic, derived from Screen-IR):";
51
+ export declare function renderBaselineText(baseline: ScreenTestBaseline): string;
52
+ //# sourceMappingURL=screenIrTestBaseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenIrTestBaseline.d.ts","sourceRoot":"","sources":["../../../src/domain/figma/screenIrTestBaseline.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAuB,QAAQ,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,MAAM,sBAAsB,GAC9B,gBAAgB,GAChB,kBAAkB,GAClB,gBAAgB,GAChB,eAAe,GACf,OAAO,GAGP,YAAY,GACZ,MAAM,GAGN,MAAM,GACN,iBAAiB,CAAC;AAEtB,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC;AAEpE,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IACzC,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CAC/C;AA2DD,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAsI9C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAKlE;AA2GD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,QAAQ,EAChB,UAAU,GAAE,SAAS,kBAAkB,EAAO,GAC7C,kBAAkB,CAMpB;AAED;;;;;GAKG;AACH;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,sEAC8B,CAAC;AAEtE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAOvE"}