@qontinui/shared-types 0.2.4 → 0.3.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 (589) hide show
  1. package/README.md +128 -128
  2. package/dist/{Region.d-BbpFAWF4.d.cts → Region.d-DtT3UphX.d.cts} +81 -81
  3. package/dist/{Region.d-BbpFAWF4.d.ts → Region.d-DtT3UphX.d.ts} +81 -81
  4. package/dist/{TaskRunFindingActionType.d-B4VmmbB6.d.cts → TaskRunFindingActionType.d-CNIWogcU.d.cts} +53 -53
  5. package/dist/{TaskRunFindingActionType.d-B4VmmbB6.d.ts → TaskRunFindingActionType.d-CNIWogcU.d.ts} +53 -53
  6. package/dist/accessibility.cjs.map +1 -1
  7. package/dist/accessibility.d.cts +379 -379
  8. package/dist/accessibility.d.ts +379 -379
  9. package/dist/chat.cjs.map +1 -1
  10. package/dist/{chunk-ZK5MJEGT.js → chunk-CTBDIP7K.js} +1 -1
  11. package/dist/chunk-CTBDIP7K.js.map +1 -0
  12. package/dist/{chunk-YXD4WCYQ.js → chunk-ZUZ3OLOH.js} +1 -1
  13. package/dist/chunk-ZUZ3OLOH.js.map +1 -0
  14. package/dist/config.cjs.map +1 -1
  15. package/dist/config.d.cts +105 -105
  16. package/dist/config.d.ts +105 -105
  17. package/dist/constraints.cjs.map +1 -1
  18. package/dist/constraints.d.cts +372 -372
  19. package/dist/constraints.d.ts +372 -372
  20. package/dist/execution.cjs.map +1 -1
  21. package/dist/execution.d.cts +869 -869
  22. package/dist/execution.d.ts +869 -869
  23. package/dist/execution.js +1 -1
  24. package/dist/geometry.cjs.map +1 -1
  25. package/dist/geometry.d.cts +81 -81
  26. package/dist/geometry.d.ts +81 -81
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +632 -590
  29. package/dist/index.d.ts +632 -590
  30. package/dist/index.js +2 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/library.cjs.map +1 -1
  33. package/dist/rag.cjs.map +1 -1
  34. package/dist/rag.d.cts +579 -579
  35. package/dist/rag.d.ts +579 -579
  36. package/dist/runner.cjs.map +1 -1
  37. package/dist/runner.d.cts +147 -147
  38. package/dist/runner.d.ts +147 -147
  39. package/dist/scheduler.cjs.map +1 -1
  40. package/dist/scheduler.d.cts +692 -692
  41. package/dist/scheduler.d.ts +692 -692
  42. package/dist/state-machine.cjs.map +1 -1
  43. package/dist/state-machine.d.cts +1128 -1128
  44. package/dist/state-machine.d.ts +1128 -1128
  45. package/dist/targets.cjs.map +1 -1
  46. package/dist/targets.d.cts +270 -270
  47. package/dist/targets.d.ts +270 -270
  48. package/dist/task-run.cjs.map +1 -1
  49. package/dist/task-run.d.cts +1431 -1431
  50. package/dist/task-run.d.ts +1431 -1431
  51. package/dist/tauri-events.cjs.map +1 -1
  52. package/dist/tauri-events.d.cts +310 -310
  53. package/dist/tauri-events.d.ts +310 -310
  54. package/dist/tree-events.cjs.map +1 -1
  55. package/dist/tree-events.d.cts +483 -483
  56. package/dist/tree-events.d.ts +483 -483
  57. package/dist/ui-bridge-ir.cjs +30 -33
  58. package/dist/ui-bridge-ir.cjs.map +1 -1
  59. package/dist/ui-bridge-ir.d.cts +98 -84
  60. package/dist/ui-bridge-ir.d.ts +98 -84
  61. package/dist/ui-bridge-ir.js +30 -33
  62. package/dist/ui-bridge-ir.js.map +1 -1
  63. package/dist/workflow.cjs.map +1 -1
  64. package/dist/workflow.d.cts +1634 -1634
  65. package/dist/workflow.d.ts +1634 -1634
  66. package/dist/workflow.js +1 -1
  67. package/package.json +141 -141
  68. package/src/accessibility/_api.ts +17 -17
  69. package/src/accessibility/index.ts +1 -1
  70. package/src/canvas/index.ts +319 -319
  71. package/src/chat/_api.ts +23 -23
  72. package/src/chat/index.ts +1 -1
  73. package/src/config/_api.ts +13 -13
  74. package/src/config/index.ts +1 -1
  75. package/src/constraints/_api.ts +17 -17
  76. package/src/constraints/index.ts +1 -1
  77. package/src/discovery.ts +188 -188
  78. package/src/execution/_api.ts +483 -483
  79. package/src/execution/index.ts +1 -1
  80. package/src/generated/A11yAction.d.ts +11 -11
  81. package/src/generated/AccessibilityBackend.d.ts +12 -12
  82. package/src/generated/AccessibilityBounds.d.ts +17 -17
  83. package/src/generated/AccessibilityNode.d.ts +73 -73
  84. package/src/generated/AccessibilityRole.d.ts +93 -93
  85. package/src/generated/AccessibilityRoleCriterion.d.ts +13 -13
  86. package/src/generated/AccessibilitySelector.d.ts +76 -76
  87. package/src/generated/AccessibilitySnapshot.d.ts +43 -43
  88. package/src/generated/AccessibilityState.d.ts +67 -67
  89. package/src/generated/ActionExecutionBatch.d.ts +23 -23
  90. package/src/generated/ActionExecutionBatchResponse.d.ts +24 -24
  91. package/src/generated/ActionExecutionCreate.d.ts +124 -124
  92. package/src/generated/ActionExecutionListResponse.d.ts +20 -20
  93. package/src/generated/ActionExecutionResponse.d.ts +24 -24
  94. package/src/generated/ActionReliabilityStats.d.ts +54 -54
  95. package/src/generated/ActionResponse.d.ts +45 -45
  96. package/src/generated/ActionStatus.d.ts +11 -11
  97. package/src/generated/ActionType.d.ts +41 -41
  98. package/src/generated/ActiveStatesResult.d.ts +32 -32
  99. package/src/generated/AgenticPhaseOutput.d.ts +50 -50
  100. package/src/generated/AgenticStatus.d.ts +11 -11
  101. package/src/generated/AiWorkflow.d.ts +76 -76
  102. package/src/generated/ApiAssertion.d.ts +35 -35
  103. package/src/generated/ApiAssertionOperator.d.ts +11 -11
  104. package/src/generated/ApiAssertionType.d.ts +11 -11
  105. package/src/generated/ApiContentType.d.ts +11 -11
  106. package/src/generated/ApiVariableExtraction.d.ts +24 -24
  107. package/src/generated/AssertionMiss.d.ts +22 -0
  108. package/src/generated/AssertionOutcome.d.ts +30 -0
  109. package/src/generated/AssertionResult.d.ts +38 -0
  110. package/src/generated/AssertionScope.d.ts +37 -0
  111. package/src/generated/AssertionSeverityCounts.d.ts +28 -0
  112. package/src/generated/AvailableTransitionsResult.d.ts +34 -34
  113. package/src/generated/BaseStepFields.d.ts +72 -72
  114. package/src/generated/BatchComputeEmbeddingRequest.d.ts +27 -27
  115. package/src/generated/BatchComputeEmbeddingResponse.d.ts +23 -23
  116. package/src/generated/BatchEmbeddingResult.d.ts +23 -23
  117. package/src/generated/BetweenIterations.d.ts +35 -35
  118. package/src/generated/BoundingBox.d.ts +18 -18
  119. package/src/generated/BridgeFingerprint.d.ts +40 -0
  120. package/src/generated/BuildPhaseConfig.d.ts +24 -24
  121. package/src/generated/BuiltinOverrideProposal.d.ts +27 -27
  122. package/src/generated/CandidateMiss.d.ts +39 -0
  123. package/src/generated/CanonicalStep.d.ts +60 -60
  124. package/src/generated/CatchUpPolicy.d.ts +12 -12
  125. package/src/generated/Category.d.ts +24 -24
  126. package/src/generated/CheckIssueDetail.d.ts +40 -40
  127. package/src/generated/CheckType.d.ts +21 -21
  128. package/src/generated/CodeExecutionStep.d.ts +88 -88
  129. package/src/generated/CommandMode.d.ts +11 -11
  130. package/src/generated/CommandStep.d.ts +183 -183
  131. package/src/generated/CommandStepPhase.d.ts +11 -11
  132. package/src/generated/ComponentActionInfo.d.ts +24 -24
  133. package/src/generated/ComponentActionRequest.d.ts +22 -22
  134. package/src/generated/ComputeEmbeddingRequest.d.ts +24 -24
  135. package/src/generated/ComputeEmbeddingResponse.d.ts +38 -38
  136. package/src/generated/ComputeTextEmbeddingRequest.d.ts +20 -20
  137. package/src/generated/ComputeTextEmbeddingResponse.d.ts +32 -32
  138. package/src/generated/ConditionScheduleConfig.d.ts +17 -17
  139. package/src/generated/ConditionStatus.d.ts +29 -29
  140. package/src/generated/Confidence.d.ts +11 -11
  141. package/src/generated/ConfidenceLevel.d.ts +11 -11
  142. package/src/generated/ConjunctEvaluation.d.ts +24 -0
  143. package/src/generated/ConjunctRule.d.ts +55 -0
  144. package/src/generated/Constraint.d.ts +33 -33
  145. package/src/generated/ConstraintCheck.d.ts +64 -64
  146. package/src/generated/ConstraintProposal.d.ts +37 -37
  147. package/src/generated/ConstraintResult.d.ts +32 -32
  148. package/src/generated/ConstraintSeverity.d.ts +16 -16
  149. package/src/generated/ConstraintViolation.d.ts +24 -24
  150. package/src/generated/Context.d.ts +50 -50
  151. package/src/generated/ContextAutoInclude.d.ts +34 -34
  152. package/src/generated/CoordinateSystem.d.ts +12 -12
  153. package/src/generated/Coordinates.d.ts +33 -33
  154. package/src/generated/CostTrendDataPoint.d.ts +36 -36
  155. package/src/generated/CostTrendResponse.d.ts +41 -41
  156. package/src/generated/CoverageData.d.ts +52 -52
  157. package/src/generated/CreateMcpCallInput.d.ts +59 -59
  158. package/src/generated/CreateMcpServerInput.d.ts +48 -48
  159. package/src/generated/CreateOlConfigRequest.d.ts +27 -27
  160. package/src/generated/CreateScheduledTaskRequest.d.ts +63 -63
  161. package/src/generated/CreateTaskRunRequest.d.ts +51 -51
  162. package/src/generated/CriterionOverride.d.ts +39 -39
  163. package/src/generated/CriterionType.d.ts +11 -11
  164. package/src/generated/DagApprovalStep.d.ts +80 -80
  165. package/src/generated/DagCancelStep.d.ts +76 -76
  166. package/src/generated/DagLoopStep.d.ts +80 -80
  167. package/src/generated/DecomposerConfig.d.ts +31 -31
  168. package/src/generated/DiagnosePhaseConfig.d.ts +34 -34
  169. package/src/generated/DiagnosticResult.d.ts +40 -40
  170. package/src/generated/DiscoveredElement.d.ts +48 -48
  171. package/src/generated/DiscoveredState.d.ts +49 -49
  172. package/src/generated/DiscoveredStateImage.d.ts +56 -56
  173. package/src/generated/DiscoveredTransition.d.ts +44 -44
  174. package/src/generated/DiscoveryBoundingBox.d.ts +32 -32
  175. package/src/generated/DiscoveryRequest.d.ts +40 -40
  176. package/src/generated/DiscoveryResponse.d.ts +32 -32
  177. package/src/generated/DiscoverySourceType.d.ts +14 -14
  178. package/src/generated/DiscoveryStrategy.d.ts +13 -13
  179. package/src/generated/DiscoveryTransitionTrigger.d.ts +38 -38
  180. package/src/generated/DisplayNode.d.ts +45 -45
  181. package/src/generated/DomainAssignment.d.ts +50 -50
  182. package/src/generated/DomainKnowledge.d.ts +29 -29
  183. package/src/generated/DomainVerificationResult.d.ts +36 -36
  184. package/src/generated/ElementActionRequest.d.ts +28 -28
  185. package/src/generated/ElementBbox.d.ts +36 -36
  186. package/src/generated/ElementIdentifier.d.ts +40 -40
  187. package/src/generated/ElementRect.d.ts +48 -48
  188. package/src/generated/ElementState.d.ts +47 -47
  189. package/src/generated/ElementType.d.ts +45 -45
  190. package/src/generated/EmbeddedElement.d.ts +36 -36
  191. package/src/generated/EmbeddingItem.d.ts +49 -49
  192. package/src/generated/EmbeddingListResponse.d.ts +22 -22
  193. package/src/generated/EmbeddingResultItem.d.ts +29 -29
  194. package/src/generated/EmbeddingResultsRequest.d.ts +19 -19
  195. package/src/generated/EmbeddingResultsResponse.d.ts +17 -17
  196. package/src/generated/ErrorType.d.ts +18 -18
  197. package/src/generated/ExecutePlaybookStep.d.ts +84 -84
  198. package/src/generated/ExecutionActionType.d.ts +40 -40
  199. package/src/generated/ExecutionIssueBatch.d.ts +19 -19
  200. package/src/generated/ExecutionIssueBatchResponse.d.ts +24 -24
  201. package/src/generated/ExecutionIssueCreate.d.ts +57 -57
  202. package/src/generated/ExecutionIssueDetail.d.ts +89 -89
  203. package/src/generated/ExecutionIssueListResponse.d.ts +26 -26
  204. package/src/generated/ExecutionIssueResponse.d.ts +24 -24
  205. package/src/generated/ExecutionIssueUpdate.d.ts +31 -31
  206. package/src/generated/ExecutionMatchLocation.d.ts +31 -31
  207. package/src/generated/ExecutionRunComplete.d.ts +34 -34
  208. package/src/generated/ExecutionRunCompleteResponse.d.ts +38 -38
  209. package/src/generated/ExecutionRunCreate.d.ts +40 -40
  210. package/src/generated/ExecutionRunDetail.d.ts +76 -76
  211. package/src/generated/ExecutionRunListResponse.d.ts +22 -22
  212. package/src/generated/ExecutionRunResponse.d.ts +41 -41
  213. package/src/generated/ExecutionScreenshotCreate.d.ts +59 -59
  214. package/src/generated/ExecutionScreenshotResponse.d.ts +36 -36
  215. package/src/generated/ExecutionStats.d.ts +56 -56
  216. package/src/generated/ExecutionStep.d.ts +78 -78
  217. package/src/generated/ExecutionTreeResponse.d.ts +42 -42
  218. package/src/generated/ExecutionTrendDataPoint.d.ts +36 -36
  219. package/src/generated/ExecutionTrendResponse.d.ts +46 -46
  220. package/src/generated/ExitCheckResult.d.ts +20 -20
  221. package/src/generated/ExitStrategy.d.ts +27 -27
  222. package/src/generated/ExportResult.d.ts +30 -30
  223. package/src/generated/ExtendIterationsRequest.d.ts +20 -20
  224. package/src/generated/FieldDiff.d.ts +33 -0
  225. package/src/generated/FileChange.d.ts +20 -20
  226. package/src/generated/Finding.d.ts +40 -40
  227. package/src/generated/FindingActionType.d.ts +13 -13
  228. package/src/generated/FindingBatchCreate.d.ts +27 -27
  229. package/src/generated/FindingCategory.d.ts +25 -25
  230. package/src/generated/FindingCodeContext.d.ts +31 -31
  231. package/src/generated/FindingCreate.d.ts +52 -52
  232. package/src/generated/FindingDetail.d.ts +78 -78
  233. package/src/generated/FindingListResponse.d.ts +40 -40
  234. package/src/generated/FindingOutput.d.ts +36 -36
  235. package/src/generated/FindingSeverity.d.ts +14 -14
  236. package/src/generated/FindingStatus.d.ts +15 -15
  237. package/src/generated/FindingSummary.d.ts +53 -53
  238. package/src/generated/FindingUpdate.d.ts +33 -33
  239. package/src/generated/FindingUserInput.d.ts +27 -27
  240. package/src/generated/FindingsSummary.d.ts +47 -47
  241. package/src/generated/FullRunnerStep.d.ts +144 -144
  242. package/src/generated/GUIElementChunk.d.ts +151 -151
  243. package/src/generated/GateEvaluationResult.d.ts +36 -36
  244. package/src/generated/HealthCheckUrl.d.ts +35 -35
  245. package/src/generated/HistoricalActionQuery.d.ts +42 -42
  246. package/src/generated/HistoricalActionResult.d.ts +57 -57
  247. package/src/generated/HttpConfig.d.ts +27 -27
  248. package/src/generated/HttpMethod.d.ts +13 -13
  249. package/src/generated/IdleCondition.d.ts +17 -17
  250. package/src/generated/ImplementFixesConfig.d.ts +25 -25
  251. package/src/generated/IndividualCheckResult.d.ts +50 -50
  252. package/src/generated/InitialStateRef.d.ts +23 -23
  253. package/src/generated/InitialStatesSource.d.ts +18 -18
  254. package/src/generated/IrAssertion.d.ts +26 -0
  255. package/src/generated/IrAssertionTarget.d.ts +28 -0
  256. package/src/generated/IrCrossRef.d.ts +16 -0
  257. package/src/generated/IrElementCriteria.d.ts +29 -0
  258. package/src/generated/IrGroup.d.ts +33 -0
  259. package/src/generated/IrMetadata.d.ts +15 -0
  260. package/src/generated/IrPageSpec.d.ts +51 -0
  261. package/src/generated/IrProvenance.d.ts +28 -0
  262. package/src/generated/IrState.d.ts +36 -0
  263. package/src/generated/IrStateCondition.d.ts +19 -0
  264. package/src/generated/IrTransition.d.ts +34 -0
  265. package/src/generated/IrTransitionAction.d.ts +17 -0
  266. package/src/generated/IrWaitSpec.d.ts +22 -0
  267. package/src/generated/IssueSeverity.d.ts +11 -11
  268. package/src/generated/IssueSource.d.ts +11 -11
  269. package/src/generated/IssueStatus.d.ts +11 -11
  270. package/src/generated/IssueType.d.ts +21 -21
  271. package/src/generated/IterationResult.d.ts +65 -65
  272. package/src/generated/IterationVerificationResults.d.ts +53 -53
  273. package/src/generated/JobItem.d.ts +31 -31
  274. package/src/generated/JobListResponse.d.ts +20 -20
  275. package/src/generated/JobStatus.d.ts +11 -11
  276. package/src/generated/JobSummary.d.ts +30 -30
  277. package/src/generated/LLMCostSummary.d.ts +38 -38
  278. package/src/generated/LLMMetrics.d.ts +42 -42
  279. package/src/generated/LegacyAssertion.d.ts +22 -0
  280. package/src/generated/LegacyAssertionTarget.d.ts +23 -0
  281. package/src/generated/LegacyGroup.d.ts +19 -0
  282. package/src/generated/LegacyProcessStep.d.ts +16 -0
  283. package/src/generated/LegacySpec.d.ts +30 -0
  284. package/src/generated/LegacyStateMachine.d.ts +17 -0
  285. package/src/generated/LegacyStateMachineState.d.ts +21 -0
  286. package/src/generated/LegacyTransition.d.ts +20 -0
  287. package/src/generated/LogSourceSelection.d.ts +25 -25
  288. package/src/generated/LoopInstanceStatus.d.ts +28 -28
  289. package/src/generated/LoopPhase.d.ts +26 -26
  290. package/src/generated/MatchAdjustment.d.ts +23 -23
  291. package/src/generated/MatchLocation.d.ts +22 -22
  292. package/src/generated/MatchMethod.d.ts +11 -11
  293. package/src/generated/MatchOutcome.d.ts +11 -0
  294. package/src/generated/MatchedElement.d.ts +28 -0
  295. package/src/generated/McpCallRecord.d.ts +35 -35
  296. package/src/generated/McpCallsResult.d.ts +44 -44
  297. package/src/generated/McpConnectionRef.d.ts +25 -25
  298. package/src/generated/McpServerConfig.d.ts +77 -77
  299. package/src/generated/McpServerStatus.d.ts +42 -42
  300. package/src/generated/McpToolCallResult.d.ts +38 -38
  301. package/src/generated/McpToolInfo.d.ts +23 -23
  302. package/src/generated/McpToolInputSchema.d.ts +34 -34
  303. package/src/generated/McpTransport.d.ts +15 -15
  304. package/src/generated/MissReason.d.ts +20 -0
  305. package/src/generated/ModelCostBreakdown.d.ts +36 -36
  306. package/src/generated/ModelOverrideConfig.d.ts +47 -47
  307. package/src/generated/Monitor.d.ts +48 -48
  308. package/src/generated/MonitorPosition.d.ts +13 -13
  309. package/src/generated/MouseButton.d.ts +13 -13
  310. package/src/generated/MultiLoopConfig.d.ts +32 -32
  311. package/src/generated/MultiLoopEntry.d.ts +32 -32
  312. package/src/generated/MultiLoopStatus.d.ts +37 -37
  313. package/src/generated/NativeAccessibilityStep.d.ts +121 -121
  314. package/src/generated/NavigationResult.d.ts +40 -40
  315. package/src/generated/NewConstraintProposal.d.ts +20 -20
  316. package/src/generated/NextTaskInfo.d.ts +24 -24
  317. package/src/generated/NodeMetadata.d.ts +47 -47
  318. package/src/generated/NodeStatus.d.ts +11 -11
  319. package/src/generated/NodeType.d.ts +11 -11
  320. package/src/generated/OcrEngine.d.ts +11 -11
  321. package/src/generated/OlConfig.d.ts +48 -48
  322. package/src/generated/OrchestrationLoopConfig.d.ts +87 -87
  323. package/src/generated/OrchestrationLoopStatus.d.ts +59 -59
  324. package/src/generated/Outcome.d.ts +15 -15
  325. package/src/generated/OutputLine.d.ts +23 -23
  326. package/src/generated/OutputStream.d.ts +11 -11
  327. package/src/generated/OverrideCollection.d.ts +18 -18
  328. package/src/generated/Pagination.d.ts +28 -28
  329. package/src/generated/ParserType.d.ts +17 -17
  330. package/src/generated/PathElement.d.ts +17 -17
  331. package/src/generated/PathfindingRequest.d.ts +24 -24
  332. package/src/generated/PathfindingResult.d.ts +30 -30
  333. package/src/generated/PathfindingStep.d.ts +36 -36
  334. package/src/generated/PatternOptions.d.ts +32 -32
  335. package/src/generated/PipelineConfig.d.ts +31 -31
  336. package/src/generated/PlaybackFrameRequest.d.ts +21 -21
  337. package/src/generated/PlaywrightExecutionMode.d.ts +11 -11
  338. package/src/generated/Point.d.ts +20 -20
  339. package/src/generated/PolicyConjunct.d.ts +22 -0
  340. package/src/generated/PolicyEvaluation.d.ts +22 -0
  341. package/src/generated/PolicyStatus.d.ts +11 -0
  342. package/src/generated/PollingConfig.d.ts +14 -14
  343. package/src/generated/ProcessConfig.d.ts +96 -96
  344. package/src/generated/ProcessState.d.ts +15 -15
  345. package/src/generated/ProcessStatus.d.ts +41 -41
  346. package/src/generated/PromptStep.d.ts +92 -92
  347. package/src/generated/PromptStepPhase.d.ts +13 -13
  348. package/src/generated/ProposalStatus.d.ts +13 -0
  349. package/src/generated/RAGDashboardStats.d.ts +26 -26
  350. package/src/generated/RagCompletionEvent.d.ts +25 -25
  351. package/src/generated/RagProcessingStatus.d.ts +11 -11
  352. package/src/generated/RagProgressEvent.d.ts +27 -27
  353. package/src/generated/ReadConfigResponse.d.ts +20 -20
  354. package/src/generated/RecommendedState.d.ts +23 -0
  355. package/src/generated/ReflectionFixOutput.d.ts +20 -20
  356. package/src/generated/Region.d.ts +39 -39
  357. package/src/generated/RepositoryInactiveCondition.d.ts +24 -24
  358. package/src/generated/RepositoryWatch.d.ts +20 -20
  359. package/src/generated/ResolvedInitialStates.d.ts +29 -29
  360. package/src/generated/ResolvedInitialStatesResult.d.ts +40 -40
  361. package/src/generated/ResourceLimits.d.ts +33 -33
  362. package/src/generated/RestartProcessStep.d.ts +88 -88
  363. package/src/generated/RetryPolicy.d.ts +24 -24
  364. package/src/generated/RetrySpec.d.ts +24 -24
  365. package/src/generated/RoleCriterion.d.ts +14 -14
  366. package/src/generated/RootCauseCategory.d.ts +18 -18
  367. package/src/generated/RoutingRule.d.ts +38 -38
  368. package/src/generated/RunPromptRequest.d.ts +56 -56
  369. package/src/generated/RunPromptResponse.d.ts +50 -50
  370. package/src/generated/RunPromptResponseData.d.ts +20 -20
  371. package/src/generated/RunStatus.d.ts +11 -11
  372. package/src/generated/RunType.d.ts +11 -11
  373. package/src/generated/Runner.d.ts +81 -81
  374. package/src/generated/RunnerCrash.d.ts +32 -32
  375. package/src/generated/RunnerFinding.d.ts +48 -48
  376. package/src/generated/RunnerFindingCodeContext.d.ts +20 -20
  377. package/src/generated/RunnerFindingUserInput.d.ts +19 -19
  378. package/src/generated/RunnerMetadata.d.ts +42 -42
  379. package/src/generated/RunnerStatus.d.ts +12 -12
  380. package/src/generated/RunnerUiError.d.ts +28 -28
  381. package/src/generated/RuntimeData.d.ts +40 -40
  382. package/src/generated/SaveWorkflowArtifactStep.d.ts +80 -80
  383. package/src/generated/ScheduleConditions.d.ts +29 -29
  384. package/src/generated/ScheduleExpression.d.ts +39 -39
  385. package/src/generated/ScheduledTask.d.ts +104 -104
  386. package/src/generated/ScheduledTaskStatus.d.ts +19 -19
  387. package/src/generated/ScheduledTaskType.d.ts +126 -126
  388. package/src/generated/SchedulerSettings.d.ts +28 -28
  389. package/src/generated/SchedulerStatus.d.ts +31 -31
  390. package/src/generated/ScreenshotAnnotation.d.ts +39 -39
  391. package/src/generated/ScreenshotAnnotationShape.d.ts +11 -11
  392. package/src/generated/ScreenshotType.d.ts +11 -11
  393. package/src/generated/ScrollDirection.d.ts +13 -13
  394. package/src/generated/SearchOptions.d.ts +33 -33
  395. package/src/generated/SearchResultItem.d.ts +19 -19
  396. package/src/generated/SearchStrategy.d.ts +14 -14
  397. package/src/generated/SemanticSearchRequest.d.ts +26 -26
  398. package/src/generated/SemanticSearchResponse.d.ts +21 -21
  399. package/src/generated/SpecCheckConfidence.d.ts +15 -0
  400. package/src/generated/SpecCheckPolicy.d.ts +23 -0
  401. package/src/generated/SpecCheckResult.d.ts +66 -0
  402. package/src/generated/SpecCheckStepConfig.d.ts +38 -0
  403. package/src/generated/SpecCheckSummary.d.ts +28 -0
  404. package/src/generated/SpecValidation.d.ts +27 -0
  405. package/src/generated/StageCondition.d.ts +37 -37
  406. package/src/generated/StageInput.d.ts +27 -27
  407. package/src/generated/StageOutput.d.ts +20 -20
  408. package/src/generated/StageTransition.d.ts +30 -30
  409. package/src/generated/StallDetectorConfig.d.ts +31 -31
  410. package/src/generated/StandardActionType.d.ts +40 -40
  411. package/src/generated/StateContext.d.ts +17 -17
  412. package/src/generated/StateDiscoveryResult.d.ts +104 -104
  413. package/src/generated/StateDiscoveryResultCreate.d.ts +65 -65
  414. package/src/generated/StateDiscoveryResultListResponse.d.ts +23 -23
  415. package/src/generated/StateDiscoveryResultSummary.d.ts +57 -57
  416. package/src/generated/StateDiscoveryResultUpdate.d.ts +47 -47
  417. package/src/generated/StateFilterItem.d.ts +18 -18
  418. package/src/generated/StateMachineConfig.d.ts +45 -45
  419. package/src/generated/StateMachineConfigCreate.d.ts +20 -20
  420. package/src/generated/StateMachineConfigFull.d.ts +67 -67
  421. package/src/generated/StateMachineConfigUpdate.d.ts +20 -20
  422. package/src/generated/StateMachineExport.d.ts +64 -64
  423. package/src/generated/StateMachineExportFormat.d.ts +39 -39
  424. package/src/generated/StateMachineImport.d.ts +25 -25
  425. package/src/generated/StateMachineState.d.ts +71 -71
  426. package/src/generated/StateMachineStateCreate.d.ts +52 -52
  427. package/src/generated/StateMachineStateUpdate.d.ts +48 -48
  428. package/src/generated/StateMachineTransition.d.ts +75 -75
  429. package/src/generated/StateMachineTransitionCreate.d.ts +53 -53
  430. package/src/generated/StateMachineTransitionUpdate.d.ts +53 -53
  431. package/src/generated/StateMatchResult.d.ts +36 -0
  432. package/src/generated/StateNodeData.d.ts +71 -71
  433. package/src/generated/StatesResponse.d.ts +19 -19
  434. package/src/generated/StdioConfig.d.ts +34 -34
  435. package/src/generated/StepExecutionConfig.d.ts +37 -37
  436. package/src/generated/StructuredFinding.d.ts +32 -32
  437. package/src/generated/StructuredOverride.d.ts +24 -24
  438. package/src/generated/StructuredSignal.d.ts +20 -20
  439. package/src/generated/SuccessCriterion.d.ts +57 -57
  440. package/src/generated/SummarizationConfig.d.ts +35 -35
  441. package/src/generated/TargetConfig.d.ts +108 -108
  442. package/src/generated/TaskCompletionResult.d.ts +90 -90
  443. package/src/generated/TaskExecutionRecord.d.ts +61 -61
  444. package/src/generated/TaskRun.d.ts +88 -88
  445. package/src/generated/TaskRunBackend.d.ts +86 -86
  446. package/src/generated/TaskRunBackendDetail.d.ts +99 -99
  447. package/src/generated/TaskRunCreate.d.ts +62 -62
  448. package/src/generated/TaskRunFilters.d.ts +38 -38
  449. package/src/generated/TaskRunFinding.d.ts +100 -100
  450. package/src/generated/TaskRunFindingActionType.d.ts +11 -11
  451. package/src/generated/TaskRunFindingCategory.d.ts +24 -24
  452. package/src/generated/TaskRunFindingCreate.d.ts +79 -79
  453. package/src/generated/TaskRunFindingFilters.d.ts +28 -28
  454. package/src/generated/TaskRunFindingSeverity.d.ts +11 -11
  455. package/src/generated/TaskRunFindingStatus.d.ts +11 -11
  456. package/src/generated/TaskRunFindingSummary.d.ts +34 -34
  457. package/src/generated/TaskRunFindingUpdate.d.ts +34 -34
  458. package/src/generated/TaskRunFindingsListResponse.d.ts +24 -24
  459. package/src/generated/TaskRunListResponse.d.ts +21 -21
  460. package/src/generated/TaskRunSession.d.ts +40 -40
  461. package/src/generated/TaskRunStatus.d.ts +11 -11
  462. package/src/generated/TaskRunUpdate.d.ts +47 -47
  463. package/src/generated/TaskType.d.ts +11 -11
  464. package/src/generated/TerminalExitEvent.d.ts +26 -26
  465. package/src/generated/TerminalInfo.d.ts +69 -69
  466. package/src/generated/TerminalOutputEvent.d.ts +29 -29
  467. package/src/generated/TestType.d.ts +11 -11
  468. package/src/generated/TextMatchType.d.ts +11 -11
  469. package/src/generated/TextSearchOptions.d.ts +30 -30
  470. package/src/generated/Ticket.d.ts +53 -53
  471. package/src/generated/TicketComment.d.ts +28 -28
  472. package/src/generated/TicketProviderConfig.d.ts +55 -55
  473. package/src/generated/TicketSource.d.ts +17 -17
  474. package/src/generated/TicketState.d.ts +15 -15
  475. package/src/generated/TimingInfo.d.ts +23 -23
  476. package/src/generated/TopMatch.d.ts +17 -17
  477. package/src/generated/TransitionAction.d.ts +93 -93
  478. package/src/generated/TransitionActionValue.d.ts +14 -14
  479. package/src/generated/TransitionEdgeData.d.ts +46 -46
  480. package/src/generated/TransitionExecutionResult.d.ts +30 -30
  481. package/src/generated/TransitionInfo.d.ts +29 -29
  482. package/src/generated/TransitionTriggerType.d.ts +13 -13
  483. package/src/generated/TreeEvent.d.ts +47 -47
  484. package/src/generated/TreeEventCreate.d.ts +30 -30
  485. package/src/generated/TreeEventListResponse.d.ts +30 -30
  486. package/src/generated/TreeEventResponse.d.ts +47 -47
  487. package/src/generated/TreeEventType.d.ts +20 -20
  488. package/src/generated/TreeNode.d.ts +54 -54
  489. package/src/generated/UIBridgeCapturedToast.d.ts +22 -22
  490. package/src/generated/UIBridgeComponent.d.ts +45 -45
  491. package/src/generated/UIBridgeElement.d.ts +90 -62
  492. package/src/generated/UIBridgeModalInfo.d.ts +40 -40
  493. package/src/generated/UIBridgeModalStack.d.ts +19 -19
  494. package/src/generated/UIBridgeSnapshot.d.ts +67 -67
  495. package/src/generated/UIBridgeToastContext.d.ts +17 -17
  496. package/src/generated/UIBridgeUndoContext.d.ts +19 -19
  497. package/src/generated/UiBridgeAction.d.ts +18 -18
  498. package/src/generated/UiBridgeAssertType.d.ts +11 -11
  499. package/src/generated/UiBridgeComparisonMode.d.ts +11 -11
  500. package/src/generated/UiBridgeDesignAuditStep.d.ts +76 -76
  501. package/src/generated/UiBridgeRequestEnvelope.d.ts +45 -45
  502. package/src/generated/UiBridgeResponseEnvelope.d.ts +66 -66
  503. package/src/generated/UiBridgeSeverity.d.ts +11 -11
  504. package/src/generated/UiBridgeStep.d.ts +127 -127
  505. package/src/generated/UiBridgeStepPhase.d.ts +14 -14
  506. package/src/generated/UiBridgeVisualAssertionStep.d.ts +97 -97
  507. package/src/generated/UnifiedStep.d.ts +57 -57
  508. package/src/generated/UnifiedWorkflow.d.ts +411 -411
  509. package/src/generated/UpdateMcpServerInput.d.ts +28 -28
  510. package/src/generated/UpdateOlConfigRequest.d.ts +32 -32
  511. package/src/generated/UpdateScheduledTaskRequest.d.ts +71 -71
  512. package/src/generated/ValidateConfigRequest.d.ts +16 -16
  513. package/src/generated/ValidateConfigResponse.d.ts +28 -28
  514. package/src/generated/VectorSearchResult.d.ts +48 -48
  515. package/src/generated/VerificationAgentContext.d.ts +25 -25
  516. package/src/generated/VerificationCategoryKind.d.ts +19 -19
  517. package/src/generated/VerificationMethod.d.ts +20 -20
  518. package/src/generated/VerificationPhaseResult.d.ts +63 -63
  519. package/src/generated/VerificationPlan.d.ts +43 -43
  520. package/src/generated/VerificationResult.d.ts +44 -44
  521. package/src/generated/VerificationResultResponse.d.ts +65 -65
  522. package/src/generated/VerificationResultsListResponse.d.ts +41 -41
  523. package/src/generated/VerificationStepDetails.d.ts +55 -55
  524. package/src/generated/VerificationStepResult.d.ts +68 -68
  525. package/src/generated/VgaAction.d.ts +61 -61
  526. package/src/generated/VgaActionKind.d.ts +14 -14
  527. package/src/generated/VgaAutomateStep.d.ts +110 -110
  528. package/src/generated/VirtualDesktop.d.ts +23 -23
  529. package/src/generated/VisualAssertionType.d.ts +11 -11
  530. package/src/generated/VisualComparisonResult.d.ts +40 -40
  531. package/src/generated/WaitOptions.d.ts +35 -35
  532. package/src/generated/WorkerCoordinationMessage.d.ts +93 -93
  533. package/src/generated/WorkerDomain.d.ts +29 -29
  534. package/src/generated/WorkerInstance.d.ts +62 -62
  535. package/src/generated/WorkerOutput.d.ts +53 -53
  536. package/src/generated/WorkerSignal.d.ts +48 -48
  537. package/src/generated/WorkerStatus.d.ts +11 -11
  538. package/src/generated/WorkflowArchitecture.d.ts +16 -16
  539. package/src/generated/WorkflowFixupMode.d.ts +11 -11
  540. package/src/generated/WorkflowFixupStep.d.ts +85 -85
  541. package/src/generated/WorkflowInfo.d.ts +28 -28
  542. package/src/generated/WorkflowMetadata.d.ts +44 -44
  543. package/src/generated/WorkflowRefStep.d.ts +94 -94
  544. package/src/generated/WorkflowStage.d.ts +126 -126
  545. package/src/generated/WorkflowStep.d.ts +80 -80
  546. package/src/generated/WorkflowStepPhase.d.ts +11 -11
  547. package/src/generated/WriteConfigRequest.d.ts +20 -20
  548. package/src/generated/WriteConfigResponse.d.ts +24 -24
  549. package/src/generated/index.ts +478 -432
  550. package/src/geometry/_api.ts +16 -16
  551. package/src/geometry/index.ts +1 -1
  552. package/src/index.ts +78 -73
  553. package/src/known-issues/index.ts +179 -179
  554. package/src/library/_api.ts +326 -326
  555. package/src/library/index.ts +1 -1
  556. package/src/rag/_api.ts +47 -47
  557. package/src/rag/index.ts +1 -1
  558. package/src/render-log.ts +371 -371
  559. package/src/runner/_api.ts +14 -14
  560. package/src/runner/index.ts +1 -1
  561. package/src/scheduler/_api.ts +21 -21
  562. package/src/scheduler/index.ts +1 -1
  563. package/src/state-machine/_api.ts +145 -145
  564. package/src/state-machine/index.ts +1 -1
  565. package/src/targets/_api.ts +47 -47
  566. package/src/targets/index.ts +1 -1
  567. package/src/task-run/_api.ts +108 -108
  568. package/src/task-run/index.ts +1 -1
  569. package/src/tauri-events/_api.ts +90 -90
  570. package/src/tauri-events/index.ts +1 -1
  571. package/src/tree-events/_api.ts +42 -42
  572. package/src/tree-events/index.ts +1 -1
  573. package/src/ui-bridge-ir/__tests__/projection.test.ts +1121 -1029
  574. package/src/ui-bridge-ir/adapter.ts +226 -220
  575. package/src/ui-bridge-ir/document.ts +114 -114
  576. package/src/ui-bridge-ir/element-criteria.ts +48 -48
  577. package/src/ui-bridge-ir/group.ts +84 -84
  578. package/src/ui-bridge-ir/index.ts +24 -24
  579. package/src/ui-bridge-ir/ir.schema.json +297 -293
  580. package/src/ui-bridge-ir/primitives.ts +110 -102
  581. package/src/ui-bridge-ir/projection.ts +898 -921
  582. package/src/ui-bridge-ir/state.ts +97 -90
  583. package/src/ui-bridge-ir/transition.ts +92 -92
  584. package/src/workflow/_api.ts +474 -474
  585. package/src/workflow/action-plan.ts +208 -208
  586. package/src/workflow/index.ts +1 -1
  587. package/src/workflow/skill.ts +152 -152
  588. package/dist/chunk-YXD4WCYQ.js.map +0 -1
  589. package/dist/chunk-ZK5MJEGT.js.map +0 -1
@@ -1,921 +1,898 @@
1
- /**
2
- * IR -> Bundled-Page Legacy Spec projection.
3
- *
4
- * Single point of translation between authoring-time IR (`IRDocument`) and the
5
- * legacy `*.spec.uibridge.json` shape that existing tooling still consumes
6
- * (`/update-spec`, runner spec drift / verify, error monitor curator,
7
- * spec experimentation, AI session). See ADR-001 (decision #6 / #7) for the
8
- * boundary rationale; section 2 of the UI Bridge redesign for the projection's
9
- * role in keeping legacy consumers working through the migration.
10
- *
11
- * The projection is a PURE FUNCTION — same input always produces byte-identical
12
- * output. Keys within objects are sorted lexicographically, arrays preserve
13
- * input order, and no timestamps / random IDs are introduced. Downstream tests
14
- * may hash the JSON output for byte-stable assertions.
15
- *
16
- * ---------------------------------------------------------------------------
17
- * Mapping rules (worked example below)
18
- * ---------------------------------------------------------------------------
19
- *
20
- * IRDocument LegacySpec
21
- * ---------- ----------
22
- * doc.id -> metadata.component
23
- * doc.metadata?.purpose ?? doc.id -> metadata.component (purpose wins if present)
24
- * doc.description ?? doc.name -> description
25
- * doc.metadata?.tags -> metadata.tags
26
- *
27
- * For each IRState:
28
- * state -> one entry in groups[]
29
- * state.id -> group.id
30
- * state.name -> group.name
31
- * state.description ??
32
- * state.metadata?.description -> group.description
33
- * "element-presence" -> group.category (default)
34
- * state.provenance?.source ??
35
- * "ai-generated" -> group.source
36
- *
37
- * For each requiredElement of state:
38
- * (state, criteria, index) -> one assertion in group.assertions[]
39
- * "${state.id}-elem-${index}" -> assertion.id
40
- * state.metadata?.description ?? -> assertion.description
41
- * "Required element ${index} ..."
42
- * "element-presence" -> assertion.category
43
- * "critical" -> assertion.severity
44
- * "exists" -> assertion.assertionType
45
- * {
46
- * type: "search",
47
- * criteria: convert(criteria), -> assertion.target
48
- * label: "Required element ..."
49
- * }
50
- * "ai-generated" -> assertion.source
51
- * false -> assertion.reviewed
52
- * true -> assertion.enabled
53
- *
54
- * If state has zero requiredElements, the projected `group.assertions[]`
55
- * is the empty array — legacy specs ship architecture-only groups with
56
- * empty assertions, and the inverse projection's identity round-trip
57
- * depends on preserving that emptiness. (Earlier versions emitted a
58
- * placeholder assertion here, which silently grew the assertion count
59
- * during round-trip and tripped `check-spec-pairing`.)
60
- *
61
- * Each IRState -> one entry in stateMachine.states[]
62
- * state.id -> sm-state.id
63
- * state.name -> sm-state.name
64
- * state.description ?? "" -> sm-state.description
65
- * state.requiredElements (converted) -> sm-state.elements
66
- * state.isInitial ??
67
- * (state.id === doc.initialState) -> sm-state.isInitial
68
- * transitions originating from state -> sm-state.transitions
69
- *
70
- * For each IRTransition:
71
- * For each fromState in transition.fromStates:
72
- * transition -> one entry in
73
- * stateMachine.states[fromState].transitions[]
74
- * transition.id -> sm-transition.id
75
- * transition.name -> sm-transition.name
76
- * transition.activateStates -> sm-transition.activateStates
77
- * transition.exitStates ?? [] -> sm-transition.deactivateStates
78
- * (exitStates.length === 0) -> sm-transition.staysVisible
79
- *
80
- * For each action in transition.actions:
81
- * action.type -> process[].action (rename type -> action)
82
- * action.target -> process[].target (criteria converted)
83
- * action.waitAfter -> process[].waitAfter (passed through)
84
- *
85
- * Element-criteria conversion (IRElementCriteria -> legacy criteria):
86
- * text -> textContent
87
- * ariaLabel -> accessibleName
88
- * attributes -> dataAttributes
89
- * (others — role, textContains, id — pass through unchanged)
90
- *
91
- * ---------------------------------------------------------------------------
92
- * Worked example
93
- * ---------------------------------------------------------------------------
94
- *
95
- * const ir: IRDocument = {
96
- * version: "1.0",
97
- * id: "active",
98
- * name: "Active Dashboard",
99
- * description: "Real-time monitoring hub",
100
- * metadata: { tags: ["monitoring", "tier-1"] },
101
- * states: [
102
- * {
103
- * id: "running",
104
- * name: "Running",
105
- * description: "Workflow executing",
106
- * requiredElements: [{ role: "button", text: "Stop" }],
107
- * isInitial: false,
108
- * },
109
- * ],
110
- * transitions: [
111
- * {
112
- * id: "running-to-idle",
113
- * name: "Stop",
114
- * fromStates: ["running"],
115
- * activateStates: ["idle"],
116
- * exitStates: ["running"],
117
- * actions: [
118
- * {
119
- * type: "click",
120
- * target: { role: "button", text: "Stop" },
121
- * waitAfter: { type: "idle", timeout: 3000 },
122
- * },
123
- * ],
124
- * },
125
- * ],
126
- * initialState: "idle",
127
- * };
128
- *
129
- * projectIRToBundledPage(ir, "Notes from author")
130
- * // -> {
131
- * // version: "1.0.0",
132
- * // description: "Real-time monitoring hub\n\nNotes from author",
133
- * // groups: [{
134
- * // id: "running",
135
- * // name: "Running",
136
- * // description: "Workflow executing",
137
- * // category: "element-presence",
138
- * // assertions: [{
139
- * // id: "running-elem-0",
140
- * // description: "Required element 0 for state Running",
141
- * // category: "element-presence",
142
- * // severity: "critical",
143
- * // assertionType: "exists",
144
- * // target: {
145
- * // type: "search",
146
- * // criteria: { role: "button", textContent: "Stop" },
147
- * // label: "Required element for Running",
148
- * // },
149
- * // source: "ai-generated",
150
- * // reviewed: false,
151
- * // enabled: true,
152
- * // }],
153
- * // source: "ai-generated",
154
- * // }],
155
- * // stateMachine: {
156
- * // states: [{
157
- * // id: "running",
158
- * // name: "Running",
159
- * // description: "Workflow executing",
160
- * // elements: [{ role: "button", textContent: "Stop" }],
161
- * // isInitial: false,
162
- * // transitions: [{
163
- * // id: "running-to-idle",
164
- * // name: "Stop",
165
- * // activateStates: ["idle"],
166
- * // deactivateStates: ["running"],
167
- * // staysVisible: false,
168
- * // process: [{
169
- * // action: "click",
170
- * // target: { role: "button", textContent: "Stop" },
171
- * // waitAfter: { type: "idle", timeout: 3000 },
172
- * // }],
173
- * // }],
174
- * // }],
175
- * // },
176
- * // metadata: { component: "active", tags: ["monitoring", "tier-1"] },
177
- * // }
178
- */
179
-
180
- import type { IRElementCriteria } from "./element-criteria";
181
- import type { IRDocument } from "./document";
182
- import type { IRAssertion, IRGroup } from "./group";
183
- import type { IRState } from "./state";
184
- import type { IRTransition, IRTransitionAction, IRWaitSpec } from "./transition";
185
-
186
- /**
187
- * Projection version. Bump on shape changes that consumers must adapt to.
188
- * Independent from `IRVersion` because the legacy spec shape and the IR shape
189
- * evolve on different cadences.
190
- */
191
- export const projectionVersion = "1.0";
192
-
193
- // ---------------------------------------------------------------------------
194
- // Legacy spec shapes — derived from active.spec.uibridge.json (the reference
195
- // the projection's output must structurally match).
196
- // ---------------------------------------------------------------------------
197
-
198
- /**
199
- * Legacy element-criteria shape (after conversion from `IRElementCriteria`).
200
- *
201
- * The differences from `IRElementCriteria`:
202
- * - `text` -> `textContent`
203
- * - `ariaLabel` -> `accessibleName` (when IR has no explicit `accessibleName`)
204
- * - `attributes` -> `dataAttributes`
205
- *
206
- * Other fields (`role`, `tagName`, `textContains`, `accessibleName`, `id`)
207
- * pass through unchanged. Index signature stays open for legacy fields the
208
- * IR doesn't yet model.
209
- */
210
- export interface LegacyCriteria {
211
- role?: string;
212
- tagName?: string;
213
- textContent?: string;
214
- textContains?: string;
215
- accessibleName?: string;
216
- id?: string;
217
- dataAttributes?: Record<string, string>;
218
- [key: string]: unknown;
219
- }
220
-
221
- /**
222
- * Legacy assertion target. Always `type: "search"` for the projection — the
223
- * legacy schema also supports point/region targets, but the IR doesn't
224
- * express those.
225
- */
226
- export interface LegacyAssertionTarget {
227
- type: "search";
228
- criteria: LegacyCriteria;
229
- label: string;
230
- }
231
-
232
- /**
233
- * Legacy assertion. Mirrors the shape used by `groups[].assertions[]` in
234
- * `*.spec.uibridge.json`.
235
- */
236
- export interface LegacyAssertion {
237
- id: string;
238
- description: string;
239
- category: string;
240
- severity: "critical" | "warning" | "info";
241
- assertionType: string;
242
- target: LegacyAssertionTarget;
243
- source: string;
244
- reviewed: boolean;
245
- enabled: boolean;
246
- precondition?: string;
247
- }
248
-
249
- /**
250
- * Legacy group. One per IR state.
251
- */
252
- export interface LegacyGroup {
253
- id: string;
254
- name: string;
255
- description: string;
256
- category: string;
257
- assertions: LegacyAssertion[];
258
- source: string;
259
- }
260
-
261
- /**
262
- * Process step within a transition. Note `action` (not `type`) — matches the
263
- * runtime engine's `TransitionAction` shape.
264
- */
265
- export interface LegacyProcessStep {
266
- action: string;
267
- target: LegacyCriteria;
268
- waitAfter?: IRWaitSpec;
269
- }
270
-
271
- /**
272
- * Legacy state-machine transition.
273
- */
274
- export interface LegacyTransition {
275
- id: string;
276
- name: string;
277
- activateStates: string[];
278
- deactivateStates: string[];
279
- staysVisible: boolean;
280
- process: LegacyProcessStep[];
281
- }
282
-
283
- /**
284
- * Legacy state-machine state.
285
- */
286
- export interface LegacyStateMachineState {
287
- id: string;
288
- name: string;
289
- description: string;
290
- elements: LegacyCriteria[];
291
- isInitial: boolean;
292
- transitions: LegacyTransition[];
293
- }
294
-
295
- /**
296
- * Legacy state-machine block.
297
- */
298
- export interface LegacyStateMachine {
299
- states: LegacyStateMachineState[];
300
- }
301
-
302
- /**
303
- * Legacy spec metadata block. The projection writes a deterministic subset;
304
- * downstream tooling may layer additional keys on hand-authored specs.
305
- */
306
- export interface LegacyMetadata {
307
- component: string;
308
- tags?: string[];
309
- [key: string]: unknown;
310
- }
311
-
312
- /**
313
- * Top-level legacy spec shape.
314
- */
315
- export interface LegacySpec {
316
- version: string;
317
- description: string;
318
- groups: LegacyGroup[];
319
- stateMachine: LegacyStateMachine;
320
- metadata: LegacyMetadata;
321
- }
322
-
323
- // ---------------------------------------------------------------------------
324
- // Internal helpers
325
- // ---------------------------------------------------------------------------
326
-
327
- /**
328
- * Recursively sort object keys lexicographically. Arrays preserve input order
329
- * (ordering is meaningful for groups / assertions / transitions).
330
- *
331
- * Used as the final stage of every projected output to guarantee byte-stable
332
- * JSON serialization downstream.
333
- */
334
- function sortKeys<T>(value: T): T {
335
- if (Array.isArray(value)) {
336
- return value.map(sortKeys) as unknown as T;
337
- }
338
- if (value !== null && typeof value === "object") {
339
- const sorted: Record<string, unknown> = {};
340
- const keys = Object.keys(value as Record<string, unknown>).sort();
341
- for (const key of keys) {
342
- sorted[key] = sortKeys((value as Record<string, unknown>)[key]);
343
- }
344
- return sorted as unknown as T;
345
- }
346
- return value;
347
- }
348
-
349
- /**
350
- * Convert IR element criteria to legacy criteria. Renames `text` ->
351
- * `textContent`, `attributes` -> `dataAttributes`. `accessibleName` (or
352
- * `ariaLabel` as a fallback) maps to legacy `accessibleName`. `tagName`,
353
- * `role`, `textContains`, `id` pass through unchanged.
354
- */
355
- function convertCriteria(criteria: IRElementCriteria): LegacyCriteria {
356
- const out: LegacyCriteria = {};
357
- if (criteria.role !== undefined) out.role = criteria.role;
358
- if (criteria.tagName !== undefined) out.tagName = criteria.tagName;
359
- if (criteria.text !== undefined) out.textContent = criteria.text;
360
- if (criteria.textContains !== undefined) out.textContains = criteria.textContains;
361
- // Prefer explicit `accessibleName` when present; fall back to `ariaLabel`
362
- // for IR documents authored against the section-1 shape.
363
- if (criteria.accessibleName !== undefined) {
364
- out.accessibleName = criteria.accessibleName;
365
- } else if (criteria.ariaLabel !== undefined) {
366
- out.accessibleName = criteria.ariaLabel;
367
- }
368
- if (criteria.id !== undefined) out.id = criteria.id;
369
- if (criteria.attributes !== undefined) out.dataAttributes = criteria.attributes;
370
- return out;
371
- }
372
-
373
- /**
374
- * Build a single legacy assertion from a state + index + criteria.
375
- *
376
- * Always produces one assertion per requiredElement; the caller is responsible
377
- * for iterating `state.requiredElements`. Empty `requiredElements` produces an
378
- * empty `assertions[]` — legitimate legacy specs (e.g. architecture-only
379
- * groups in `graphql-infrastructure`, `activity-timeline`, `opik-integration`,
380
- * `watchers`) ship with empty `assertions[]`, and the inverse projection
381
- * round-trip must preserve that emptiness or `check-spec-pairing` flags an
382
- * assertion-count drift.
383
- */
384
- function buildAssertion(
385
- state: IRState,
386
- index: number,
387
- criteria: IRElementCriteria,
388
- ): LegacyAssertion {
389
- const description =
390
- state.metadata?.description ?? `Required element ${index} for state ${state.name}`;
391
- const targetCriteria: LegacyCriteria = convertCriteria(criteria);
392
- const assertion: LegacyAssertion = {
393
- id: `${state.id}-elem-${index}`,
394
- description,
395
- category: "element-presence",
396
- severity: "critical",
397
- assertionType: "exists",
398
- target: {
399
- type: "search",
400
- criteria: targetCriteria,
401
- label: `Required element for ${state.name}`,
402
- },
403
- source: "ai-generated",
404
- reviewed: false,
405
- enabled: true,
406
- };
407
- if (state.precondition !== undefined) {
408
- assertion.precondition = state.precondition;
409
- }
410
- return assertion;
411
- }
412
-
413
- /**
414
- * Build the legacy `groups[]` entry for one IR state. Emits exactly one
415
- * assertion per `requiredElement`; states with zero requiredElements project
416
- * to a group with an empty `assertions[]` array. Legacy consumers (runner
417
- * spec drift / verify, error monitor curator, AI session) tolerate empty
418
- * groups — multiple shipping legacy specs already use them for
419
- * architecture-only / documentation-only groups.
420
- */
421
- function buildGroup(state: IRState): LegacyGroup {
422
- const elems = state.requiredElements ?? [];
423
- const assertions: LegacyAssertion[] = elems.map((c, i) =>
424
- buildAssertion(state, i, c),
425
- );
426
- return {
427
- id: state.id,
428
- name: state.name,
429
- description: state.description ?? state.metadata?.description ?? "",
430
- category: "element-presence",
431
- assertions,
432
- source: state.provenance?.source ?? "ai-generated",
433
- };
434
- }
435
-
436
- /**
437
- * Convert an `IRAssertion` (IR-side, free-form criteria) into a `LegacyAssertion`.
438
- * The shapes are near-identical; the only meaningful difference is that the
439
- * IR-side `target.criteria` is `Record<string, unknown>` while the legacy
440
- * shape is `LegacyCriteria` (a typed but extensible record). We pass the
441
- * criteria object through verbatim — the open `[key: string]: unknown` index
442
- * signature on `LegacyCriteria` accepts any record shape.
443
- */
444
- function synthesizedAssertionToLegacy(assertion: IRAssertion): LegacyAssertion {
445
- const target: LegacyAssertionTarget = {
446
- type: "search",
447
- criteria: assertion.target.criteria as LegacyCriteria,
448
- label: assertion.target.label,
449
- };
450
- const out: LegacyAssertion = {
451
- id: assertion.id,
452
- description: assertion.description,
453
- category: assertion.category,
454
- severity: assertion.severity as LegacyAssertion["severity"],
455
- assertionType: assertion.assertionType,
456
- target,
457
- source: assertion.source,
458
- reviewed: assertion.reviewed,
459
- enabled: assertion.enabled,
460
- };
461
- if (assertion.precondition !== undefined) {
462
- out.precondition = assertion.precondition;
463
- }
464
- return out;
465
- }
466
-
467
- /**
468
- * Convert a synthesized `IRGroup` into a `LegacyGroup`. Mirrors the legacy
469
- * shape one-to-one (modulo the free-form criteria pass-through described in
470
- * `synthesizedAssertionToLegacy`). The synthesized group always carries a
471
- * `source` (synthesis emits `"ai-generated"`); fall back to `"ai-generated"`
472
- * for hand-written IR documents that omit it.
473
- */
474
- function synthesizedGroupToLegacy(group: IRGroup): LegacyGroup {
475
- return {
476
- id: group.id,
477
- name: group.name,
478
- description: group.description,
479
- category: group.category,
480
- assertions: group.assertions.map(synthesizedAssertionToLegacy),
481
- source: group.source ?? "ai-generated",
482
- };
483
- }
484
-
485
- /**
486
- * Convert an IR transition action into the legacy `process[]` step shape.
487
- * Renames IR's `type` to legacy `action` (matches the runtime engine
488
- * convention — same as the IR -> WorkflowConfig adapter does for runtime).
489
- */
490
- function buildProcessStep(action: IRTransitionAction): LegacyProcessStep {
491
- const out: LegacyProcessStep = {
492
- action: action.type,
493
- target: convertCriteria(action.target),
494
- };
495
- if (action.waitAfter !== undefined) out.waitAfter = action.waitAfter;
496
- return out;
497
- }
498
-
499
- /**
500
- * Convert one IR transition into the legacy state-machine transition shape.
501
- * Each fromState gets its own copy in the projected output (legacy specs put
502
- * the transition under each originating state's `transitions[]`).
503
- */
504
- function buildTransition(transition: IRTransition): LegacyTransition {
505
- const exitStates = transition.exitStates ?? [];
506
- return {
507
- id: transition.id,
508
- name: transition.name,
509
- activateStates: transition.activateStates,
510
- deactivateStates: exitStates,
511
- // Modal-style transitions (no exit states) stay visible behind the
512
- // activated state. Otherwise the originating state(s) deactivate, so the
513
- // page no longer shows them.
514
- staysVisible: exitStates.length === 0,
515
- process: transition.actions.map(buildProcessStep),
516
- };
517
- }
518
-
519
- /**
520
- * Build the legacy `stateMachine.states[]` entry for one IR state. Includes
521
- * any transitions that originate from this state (i.e. where state.id is in
522
- * transition.fromStates).
523
- */
524
- function buildStateMachineState(
525
- state: IRState,
526
- transitions: IRTransition[],
527
- doc: IRDocument,
528
- ): LegacyStateMachineState {
529
- const outgoing = transitions
530
- .filter((t) => t.fromStates.includes(state.id))
531
- .map(buildTransition);
532
- const isInitial =
533
- state.isInitial !== undefined ? state.isInitial : state.id === doc.initialState;
534
- return {
535
- id: state.id,
536
- name: state.name,
537
- description: state.description ?? "",
538
- elements: (state.requiredElements ?? []).map(convertCriteria),
539
- isInitial,
540
- transitions: outgoing,
541
- };
542
- }
543
-
544
- // ---------------------------------------------------------------------------
545
- // Public entry point
546
- // ---------------------------------------------------------------------------
547
-
548
- /**
549
- * Project an IR document into the legacy bundled-page spec shape.
550
- *
551
- * Pure / deterministic: same input always produces structurally identical
552
- * output (and byte-identical when JSON-serialized via the standard
553
- * `JSON.stringify` ordering). Arrays preserve input order; object keys are
554
- * sorted lexicographically.
555
- *
556
- * @param doc The IR document to project.
557
- * @param notes Optional human-authored notes (carried in the page's
558
- * `notes.md` companion file). Appended to `description` as a
559
- * separate paragraph (separator: two newlines).
560
- */
561
- export function projectIRToBundledPage(doc: IRDocument, notes?: string): LegacySpec {
562
- const baseDescription = doc.description ?? doc.name;
563
- const description =
564
- notes !== undefined && notes.length > 0
565
- ? `${baseDescription}\n\n${notes}`
566
- : baseDescription;
567
-
568
- const component = doc.metadata?.purpose ?? doc.id;
569
- const metadata: LegacyMetadata = { component };
570
- if (doc.metadata?.tags !== undefined) metadata.tags = doc.metadata.tags;
571
-
572
- const groups: LegacyGroup[] = doc.states.map(buildGroup);
573
- if (doc.synthesizedGroups !== undefined) {
574
- // Append synthesis-produced groups AFTER state-derived ones, in declared
575
- // order. Byte-stable output (see file header) requires this exact ordering
576
- // — anything else breaks `check-spec-pairing` style hashes.
577
- for (const g of doc.synthesizedGroups) {
578
- groups.push(synthesizedGroupToLegacy(g));
579
- }
580
- }
581
- const smStates: LegacyStateMachineState[] = doc.states.map((s) =>
582
- buildStateMachineState(s, doc.transitions, doc),
583
- );
584
-
585
- const spec: LegacySpec = {
586
- version: "1.0.0",
587
- description,
588
- groups,
589
- stateMachine: { states: smStates },
590
- metadata,
591
- };
592
-
593
- return sortKeys(spec);
594
- }
595
-
596
- // ---------------------------------------------------------------------------
597
- // Inverse projection — Legacy spec -> IR document
598
- // ---------------------------------------------------------------------------
599
-
600
- /**
601
- * Options for the inverse projection.
602
- *
603
- * - `docId` — explicit IRDocument.id override. Falls back to
604
- * `legacy.metadata.component` (or `fallbackName` if absent).
605
- * - `fallbackName` — used when `legacy.metadata.component` is missing AND no
606
- * `docId` was supplied.
607
- */
608
- export interface ProjectLegacyToIROptions {
609
- docId?: string;
610
- fallbackName?: string;
611
- }
612
-
613
- /**
614
- * Best-effort field-by-field invert of `LegacyCriteria` back into
615
- * `IRElementCriteria`.
616
- *
617
- * Rename map (the structural inverse of `convertCriteria`):
618
- * textContent -> text
619
- * accessibleName -> accessibleName (passes through; the forward direction
620
- * collapses ariaLabel into accessibleName,
621
- * so we cannot reliably distinguish them
622
- * on the inverse — pick accessibleName).
623
- * dataAttributes -> attributes
624
- * role / tagName / textContains / id pass through unchanged.
625
- *
626
- * Unknown legacy keys are dropped (lossy, but documented). Document-level
627
- * unknown criteria are rare; if they show up we'll surface them in Phase A4.
628
- */
629
- function invertCriteria(criteria: LegacyCriteria | undefined): IRElementCriteria {
630
- if (criteria === undefined || criteria === null) return {};
631
- const out: IRElementCriteria = {};
632
- if (criteria.role !== undefined) out.role = criteria.role;
633
- if (criteria.tagName !== undefined) out.tagName = criteria.tagName;
634
- if (criteria.textContent !== undefined) out.text = criteria.textContent;
635
- if (criteria.textContains !== undefined) out.textContains = criteria.textContains;
636
- if (criteria.accessibleName !== undefined) out.accessibleName = criteria.accessibleName;
637
- if (criteria.id !== undefined) out.id = criteria.id;
638
- if (criteria.dataAttributes !== undefined) out.attributes = criteria.dataAttributes;
639
- return out;
640
- }
641
-
642
- /**
643
- * Pick the first non-empty `precondition` across a group's assertions. Legacy
644
- * groups carry preconditions on individual assertions; IR carries it on the
645
- * state. If multiple distinct preconditions exist we keep the first
646
- * (deterministic, mirrors input ordering) — Phase A4 review will surface
647
- * conflicts for hand-fix.
648
- */
649
- function firstPrecondition(group: LegacyGroup): string | undefined {
650
- for (const a of group.assertions) {
651
- if (a.precondition !== undefined && a.precondition.length > 0) {
652
- return a.precondition;
653
- }
654
- }
655
- return undefined;
656
- }
657
-
658
- /**
659
- * Build an `IRState` from one legacy group + its corresponding state-machine
660
- * state (when present).
661
- *
662
- * IMPORTANT: `requiredElements` is sourced from the GROUP's assertion criteria
663
- * (one criteria per assertion, in input order), NOT from `smState.elements`.
664
- * This preserves the round-trip identity for the assertion-count check —
665
- * legacy specs sometimes carry an SM block whose `elements[]` is a coarse
666
- * page-state fingerprint (1-2 entries) while the group has many assertions.
667
- * Using `smState.elements` would silently collapse those into one
668
- * requiredElement per state, then the forward direction would emit only one
669
- * assertion per group.
670
- *
671
- * `smState` (when present and matched by id) contributes:
672
- * - `name` override (legacy `smState.name` is human-readable; group.name is
673
- * also human-readable but may differ — prefer smState's name since the
674
- * forward direction's `buildStateMachineState` echoes `state.name` to
675
- * `smState.name`).
676
- * - `description` fallback (when group.description is empty).
677
- * - `isInitial` (the only place this lives in legacy specs).
678
- */
679
- function buildIRState(
680
- group: LegacyGroup,
681
- smState: LegacyStateMachineState | undefined,
682
- ): IRState {
683
- // Always one requiredElement per assertion so the forward projection emits
684
- // the same number of assertions on the round-trip.
685
- const requiredElements: IRElementCriteria[] = group.assertions.map((a) =>
686
- invertCriteria(a.target?.criteria),
687
- );
688
-
689
- const description =
690
- group.description !== undefined && group.description.length > 0
691
- ? group.description
692
- : smState?.description;
693
- const precondition = firstPrecondition(group);
694
-
695
- const state: IRState = {
696
- id: group.id,
697
- name: smState?.name ?? group.name,
698
- requiredElements,
699
- };
700
- if (description !== undefined && description.length > 0) {
701
- state.description = description;
702
- }
703
- if (smState?.isInitial === true) {
704
- state.isInitial = true;
705
- } else if (smState?.isInitial === false) {
706
- state.isInitial = false;
707
- }
708
- if (precondition !== undefined) {
709
- state.precondition = precondition;
710
- }
711
- state.provenance = { source: "migrated" };
712
- return state;
713
- }
714
-
715
- /**
716
- * Convert one legacy `process[]` step back into `IRTransitionAction`. Renames
717
- * `action` -> `type`. Legacy specs occasionally omit a target on no-op steps;
718
- * we coerce to an empty criteria object in that case.
719
- */
720
- function invertProcessStep(step: LegacyProcessStep): IRTransitionAction {
721
- const out: IRTransitionAction = {
722
- type: step.action,
723
- target: invertCriteria(step.target),
724
- };
725
- if (step.waitAfter !== undefined) out.waitAfter = step.waitAfter;
726
- return out;
727
- }
728
-
729
- /**
730
- * Collapse duplicate transitions across multiple originating states. The
731
- * forward projection emits the same transition under EACH `fromState`'s
732
- * `transitions[]` entry; the inverse must merge them back into ONE
733
- * `IRTransition` whose `fromStates` lists every originator.
734
- *
735
- * Same-id transitions are assumed to be byte-identical except for the
736
- * (implicit) originating state. If they differ in `process[]` /
737
- * `activateStates` / `deactivateStates`, we keep the first occurrence and
738
- * append the additional fromState. (Genuine divergence here is a defect to
739
- * surface in Phase A4 review.)
740
- */
741
- function buildIRTransitions(stateMachine: LegacyStateMachine | undefined): IRTransition[] {
742
- if (stateMachine === undefined || !Array.isArray(stateMachine.states)) return [];
743
- const byId = new Map<string, IRTransition>();
744
- // Preserve insertion order — first state's transitions come first.
745
- const order: string[] = [];
746
-
747
- for (const smState of stateMachine.states) {
748
- const transitions = Array.isArray(smState.transitions) ? smState.transitions : [];
749
- for (const t of transitions) {
750
- const existing = byId.get(t.id);
751
- if (existing !== undefined) {
752
- if (!existing.fromStates.includes(smState.id)) {
753
- existing.fromStates.push(smState.id);
754
- }
755
- continue;
756
- }
757
- const ir: IRTransition = {
758
- id: t.id,
759
- name: t.name,
760
- fromStates: [smState.id],
761
- activateStates: Array.isArray(t.activateStates) ? [...t.activateStates] : [],
762
- actions: Array.isArray(t.process) ? t.process.map(invertProcessStep) : [],
763
- };
764
- // The forward direction maps `exitStates` -> `deactivateStates`; preserve
765
- // the field whenever it's present in legacy form (even if empty array)
766
- // so modal-style transitions (staysVisible: true, deactivateStates: [])
767
- // round-trip predictably.
768
- if (Array.isArray(t.deactivateStates)) {
769
- ir.exitStates = [...t.deactivateStates];
770
- }
771
- ir.provenance = { source: "migrated" };
772
- byId.set(t.id, ir);
773
- order.push(t.id);
774
- }
775
- }
776
-
777
- return order.map((id) => byId.get(id)!);
778
- }
779
-
780
- /**
781
- * Project a legacy bundled-page spec back into IR.
782
- *
783
- * Pure / deterministic: same input always produces structurally identical
784
- * output. Collapses transition duplicates (forward emits one copy per
785
- * `fromState`), preserves group/assertion ordering, and tags every node with
786
- * `provenance.source = "migrated"` so downstream tooling can distinguish
787
- * migrated content from hand-authored.
788
- *
789
- * The mapping is the structural inverse of `projectIRToBundledPage`:
790
- *
791
- * LegacySpec IRDocument
792
- * ---------- ----------
793
- * metadata.component -> id (or opts.docId override)
794
- * metadata.tags -> metadata.tags
795
- * description -> description (kept verbatim;
796
- * authored notes appended via
797
- * forward `notes` arg are not
798
- * stripped — that round-trip is
799
- * best-effort and Phase A4 will
800
- * split them back out)
801
- *
802
- * groups[] -> states[] (one per group)
803
- * group.id / name / description -> state.id / name / description
804
- * assertion.target.criteria -> state.requiredElements[i] (or,
805
- * preferred, smState.elements when
806
- * the stateMachine block is present)
807
- * first-found assertion.precondition -> state.precondition
808
- *
809
- * stateMachine.states[] -> contributes isInitial + elements
810
- * smState.transitions[] -> transitions[] (deduped by id)
811
- * transition.activateStates -> transition.activateStates
812
- * transition.deactivateStates -> transition.exitStates
813
- * transition.process[] -> transition.actions[]
814
- * step.action -> action.type
815
- * step.target -> action.target (criteria inverted)
816
- * step.waitAfter -> action.waitAfter
817
- *
818
- * Documented losses (acceptable for Phase A2):
819
- * - Legacy assertion ids (`{stateId}-elem-{i}`) are NOT preserved across the
820
- * round-trip — the forward direction regenerates them from index, so any
821
- * hand-edited ids are lost. See Phase A4.
822
- * - Legacy `assertionType` / `severity` / `category` / `source` per
823
- * assertion are reduced to defaults on round-trip.
824
- * - Legacy `expected` / `assertionType: "count"` and `notExists` semantics
825
- * are not modeled in IR — the assertion's criteria still survive (as a
826
- * state requiredElement) but the assertion-flavor metadata is dropped.
827
- * - Legacy `testing` block + extra metadata keys outside `component` /
828
- * `tags` are ignored. Authors keep them via the legacy spec file directly
829
- * during Phase A2 — they will move into IR companion files in Phase A4.
830
- * - The forward `notes` argument's appended paragraph is currently kept
831
- * in `description`; Phase A4 will split it back out into a dedicated
832
- * companion file.
833
- *
834
- * @param legacy The legacy spec to invert.
835
- * @param opts Optional doc-level overrides.
836
- */
837
- export function projectLegacyToIR(
838
- legacy: LegacySpec,
839
- opts?: ProjectLegacyToIROptions,
840
- ): IRDocument {
841
- const component =
842
- opts?.docId ??
843
- legacy.metadata?.component ??
844
- opts?.fallbackName ??
845
- "legacy-spec";
846
- const name = legacy.metadata?.component ?? opts?.fallbackName ?? component;
847
-
848
- // Index state-machine states by id for quick lookup when building IR states
849
- // out of groups. Older specs may not have a stateMachine block.
850
- const smByGroupId = new Map<string, LegacyStateMachineState>();
851
- if (legacy.stateMachine !== undefined && Array.isArray(legacy.stateMachine.states)) {
852
- for (const s of legacy.stateMachine.states) {
853
- smByGroupId.set(s.id, s);
854
- }
855
- }
856
-
857
- const groups = Array.isArray(legacy.groups) ? legacy.groups : [];
858
- const states: IRState[] = groups.map((group) =>
859
- buildIRState(group, smByGroupId.get(group.id)),
860
- );
861
-
862
- // NOTE: Some legacy specs declare `stateMachine.states[]` whose ids do NOT
863
- // correspond to any `groups[]` entry (e.g., page-level states like
864
- // `wrappers-installed-tab` vs assertion-category groups like
865
- // `page-structure`). The IR's group-state mapping is 1:1 with groups by
866
- // construction (the forward projection emits one group per IR state), so
867
- // these orphan SM states cannot survive the round-trip cleanly. They are
868
- // dropped here as a documented loss — Phase A4 will revisit by either
869
- // (a) introducing a separate IR concept for "page state" vs
870
- // "assertion group", or (b) hand-merging the legacy specs so groups and
871
- // SM states share ids.
872
- // Transitions originating from orphan SM states are likewise omitted from
873
- // IR (their fromStates would dangle). We filter them below.
874
- const groupIds = new Set(groups.map((g) => g.id));
875
-
876
- // Build transitions only from SM states whose ids appear in groups[]. Any
877
- // transition with a fromState that isn't a known group is dropped — the
878
- // forward direction would have nowhere to emit it.
879
- let filteredStateMachine: LegacyStateMachine | undefined = legacy.stateMachine;
880
- if (
881
- legacy.stateMachine !== undefined &&
882
- Array.isArray(legacy.stateMachine.states) &&
883
- legacy.stateMachine.states.some((s) => !groupIds.has(s.id))
884
- ) {
885
- filteredStateMachine = {
886
- states: legacy.stateMachine.states.filter((s) => groupIds.has(s.id)),
887
- };
888
- }
889
- const transitions = buildIRTransitions(filteredStateMachine);
890
-
891
- // Initial state: first state with isInitial:true wins (mirrors how the
892
- // forward direction picks the document's `initialState`). Only consider
893
- // SM states that correspond to a group (so initialState references a real
894
- // IR state).
895
- let initialState: string | undefined;
896
- if (legacy.stateMachine !== undefined && Array.isArray(legacy.stateMachine.states)) {
897
- const initial = legacy.stateMachine.states.find(
898
- (s) => s.isInitial === true && groupIds.has(s.id),
899
- );
900
- if (initial !== undefined) initialState = initial.id;
901
- }
902
-
903
- const doc: IRDocument = {
904
- version: "1.0",
905
- id: component,
906
- name,
907
- states,
908
- transitions,
909
- provenance: { source: "migrated" },
910
- };
911
- if (legacy.description !== undefined && legacy.description.length > 0) {
912
- doc.description = legacy.description;
913
- }
914
- if (legacy.metadata !== undefined && legacy.metadata.tags !== undefined) {
915
- doc.metadata = { tags: legacy.metadata.tags };
916
- }
917
- if (initialState !== undefined) {
918
- doc.initialState = initialState;
919
- }
920
- return doc;
921
- }
1
+ /**
2
+ * IR -> Bundled-Page Legacy Spec projection.
3
+ *
4
+ * Single point of translation between authoring-time IR (`IRDocument`) and the
5
+ * legacy `*.spec.uibridge.json` shape that existing tooling still consumes
6
+ * (`/update-spec`, runner spec drift / verify, error monitor curator,
7
+ * spec experimentation, AI session). See ADR-001 (decision #6 / #7) for the
8
+ * boundary rationale; section 2 of the UI Bridge redesign for the projection's
9
+ * role in keeping legacy consumers working through the migration.
10
+ *
11
+ * The projection is a PURE FUNCTION — same input always produces byte-identical
12
+ * output. Keys within objects are sorted lexicographically, arrays preserve
13
+ * input order, and no timestamps / random IDs are introduced. Downstream tests
14
+ * may hash the JSON output for byte-stable assertions.
15
+ *
16
+ * ---------------------------------------------------------------------------
17
+ * Mapping rules (worked example below)
18
+ * ---------------------------------------------------------------------------
19
+ *
20
+ * IRDocument LegacySpec
21
+ * ---------- ----------
22
+ * doc.id -> metadata.component
23
+ * doc.metadata?.purpose ?? doc.id -> metadata.component (purpose wins if present)
24
+ * doc.description ?? doc.name -> description
25
+ * doc.metadata?.tags -> metadata.tags
26
+ *
27
+ * For each IRState:
28
+ * state -> one entry in groups[]
29
+ * state.id -> group.id
30
+ * state.name -> group.name
31
+ * state.description ??
32
+ * state.metadata?.description -> group.description
33
+ * "element-presence" -> group.category (default)
34
+ * state.provenance?.source ??
35
+ * "ai-generated" -> group.source
36
+ *
37
+ * For each requiredElement of state:
38
+ * (state, criteria, index) -> one assertion in group.assertions[]
39
+ * "${state.id}-elem-${index}" -> assertion.id
40
+ * state.metadata?.description ?? -> assertion.description
41
+ * "Required element ${index} ..."
42
+ * "element-presence" -> assertion.category
43
+ * "critical" -> assertion.severity
44
+ * "exists" -> assertion.assertionType
45
+ * {
46
+ * type: "search",
47
+ * criteria: convert(criteria), -> assertion.target
48
+ * label: "Required element ..."
49
+ * }
50
+ * "ai-generated" -> assertion.source
51
+ * false -> assertion.reviewed
52
+ * true -> assertion.enabled
53
+ *
54
+ * If state has zero requiredElements, the projected `group.assertions[]`
55
+ * is the empty array — legacy specs ship architecture-only groups with
56
+ * empty assertions, and the inverse projection's identity round-trip
57
+ * depends on preserving that emptiness. (Earlier versions emitted a
58
+ * placeholder assertion here, which silently grew the assertion count
59
+ * during round-trip and tripped `check-spec-pairing`.)
60
+ *
61
+ * Each IRState -> one entry in stateMachine.states[]
62
+ * state.id -> sm-state.id
63
+ * state.name -> sm-state.name
64
+ * state.description ?? "" -> sm-state.description
65
+ * state.requiredElements (converted) -> sm-state.elements
66
+ * state.isInitial ??
67
+ * (state.id === doc.initialState) -> sm-state.isInitial
68
+ * transitions originating from state -> sm-state.transitions
69
+ *
70
+ * For each IRTransition:
71
+ * For each fromState in transition.fromStates:
72
+ * transition -> one entry in
73
+ * stateMachine.states[fromState].transitions[]
74
+ * transition.id -> sm-transition.id
75
+ * transition.name -> sm-transition.name
76
+ * transition.activateStates -> sm-transition.activateStates
77
+ * transition.exitStates ?? [] -> sm-transition.deactivateStates
78
+ * (exitStates.length === 0) -> sm-transition.staysVisible
79
+ *
80
+ * For each action in transition.actions:
81
+ * action.type -> process[].action (rename type -> action)
82
+ * action.target -> process[].target (criteria converted)
83
+ * action.waitAfter -> process[].waitAfter (passed through)
84
+ *
85
+ * Element-criteria conversion (IRElementCriteria -> legacy criteria):
86
+ * text -> textContent
87
+ * ariaLabel -> accessibleName
88
+ * attributes -> dataAttributes
89
+ * (others — role, textContains, id — pass through unchanged)
90
+ *
91
+ * ---------------------------------------------------------------------------
92
+ * Worked example
93
+ * ---------------------------------------------------------------------------
94
+ *
95
+ * const ir: IRDocument = {
96
+ * version: "1.0",
97
+ * id: "active",
98
+ * name: "Active Dashboard",
99
+ * description: "Real-time monitoring hub",
100
+ * metadata: { tags: ["monitoring", "tier-1"] },
101
+ * states: [
102
+ * {
103
+ * id: "running",
104
+ * name: "Running",
105
+ * description: "Workflow executing",
106
+ * requiredElements: [{ role: "button", text: "Stop" }],
107
+ * isInitial: false,
108
+ * },
109
+ * ],
110
+ * transitions: [
111
+ * {
112
+ * id: "running-to-idle",
113
+ * name: "Stop",
114
+ * fromStates: ["running"],
115
+ * activateStates: ["idle"],
116
+ * exitStates: ["running"],
117
+ * actions: [
118
+ * {
119
+ * type: "click",
120
+ * target: { role: "button", text: "Stop" },
121
+ * waitAfter: { type: "idle", timeout: 3000 },
122
+ * },
123
+ * ],
124
+ * },
125
+ * ],
126
+ * initialState: "idle",
127
+ * };
128
+ *
129
+ * projectIRToBundledPage(ir, "Notes from author")
130
+ * // -> {
131
+ * // version: "1.0.0",
132
+ * // description: "Real-time monitoring hub\n\nNotes from author",
133
+ * // groups: [{
134
+ * // id: "running",
135
+ * // name: "Running",
136
+ * // description: "Workflow executing",
137
+ * // category: "element-presence",
138
+ * // assertions: [{
139
+ * // id: "running-elem-0",
140
+ * // description: "Required element 0 for state Running",
141
+ * // category: "element-presence",
142
+ * // severity: "critical",
143
+ * // assertionType: "exists",
144
+ * // target: {
145
+ * // type: "search",
146
+ * // criteria: { role: "button", textContent: "Stop" },
147
+ * // label: "Required element for Running",
148
+ * // },
149
+ * // source: "ai-generated",
150
+ * // reviewed: false,
151
+ * // enabled: true,
152
+ * // }],
153
+ * // source: "ai-generated",
154
+ * // }],
155
+ * // stateMachine: {
156
+ * // states: [{
157
+ * // id: "running",
158
+ * // name: "Running",
159
+ * // description: "Workflow executing",
160
+ * // elements: [{ role: "button", textContent: "Stop" }],
161
+ * // isInitial: false,
162
+ * // transitions: [{
163
+ * // id: "running-to-idle",
164
+ * // name: "Stop",
165
+ * // activateStates: ["idle"],
166
+ * // deactivateStates: ["running"],
167
+ * // staysVisible: false,
168
+ * // process: [{
169
+ * // action: "click",
170
+ * // target: { role: "button", textContent: "Stop" },
171
+ * // waitAfter: { type: "idle", timeout: 3000 },
172
+ * // }],
173
+ * // }],
174
+ * // }],
175
+ * // },
176
+ * // metadata: { component: "active", tags: ["monitoring", "tier-1"] },
177
+ * // }
178
+ */
179
+
180
+ import type { IRElementCriteria } from "./element-criteria";
181
+ import type { IRDocument } from "./document";
182
+ import type { IRAssertion, IRGroup } from "./group";
183
+ import type { IRState } from "./state";
184
+ import type { IRTransition, IRTransitionAction, IRWaitSpec } from "./transition";
185
+
186
+ /**
187
+ * Projection version. Bump on shape changes that consumers must adapt to.
188
+ * Independent from `IRVersion` because the legacy spec shape and the IR shape
189
+ * evolve on different cadences.
190
+ */
191
+ export const projectionVersion = "1.0";
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // Legacy spec shapes — derived from active.spec.uibridge.json (the reference
195
+ // the projection's output must structurally match).
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Legacy element-criteria shape (after conversion from `IRElementCriteria`).
200
+ *
201
+ * The differences from `IRElementCriteria`:
202
+ * - `text` -> `textContent`
203
+ * - `ariaLabel` -> `accessibleName` (when IR has no explicit `accessibleName`)
204
+ * - `attributes` -> `dataAttributes`
205
+ *
206
+ * Other fields (`role`, `tagName`, `textContains`, `accessibleName`, `id`)
207
+ * pass through unchanged. Index signature stays open for legacy fields the
208
+ * IR doesn't yet model.
209
+ */
210
+ export interface LegacyCriteria {
211
+ role?: string;
212
+ tagName?: string;
213
+ textContent?: string;
214
+ textContains?: string;
215
+ accessibleName?: string;
216
+ id?: string;
217
+ dataAttributes?: Record<string, string>;
218
+ [key: string]: unknown;
219
+ }
220
+
221
+ /**
222
+ * Legacy assertion target. Always `type: "search"` for the projection — the
223
+ * legacy schema also supports point/region targets, but the IR doesn't
224
+ * express those.
225
+ */
226
+ export interface LegacyAssertionTarget {
227
+ type: "search";
228
+ criteria: LegacyCriteria;
229
+ label: string;
230
+ }
231
+
232
+ /**
233
+ * Legacy assertion. Mirrors the shape used by `groups[].assertions[]` in
234
+ * `*.spec.uibridge.json`.
235
+ */
236
+ export interface LegacyAssertion {
237
+ id: string;
238
+ description: string;
239
+ category: string;
240
+ severity: "critical" | "warning" | "info";
241
+ assertionType: string;
242
+ target: LegacyAssertionTarget;
243
+ source: string;
244
+ reviewed: boolean;
245
+ enabled: boolean;
246
+ precondition?: string;
247
+ }
248
+
249
+ /**
250
+ * Legacy group. One per IR state.
251
+ */
252
+ export interface LegacyGroup {
253
+ id: string;
254
+ name: string;
255
+ description: string;
256
+ category: string;
257
+ assertions: LegacyAssertion[];
258
+ source: string;
259
+ }
260
+
261
+ /**
262
+ * Process step within a transition. Note `action` (not `type`) — matches the
263
+ * runtime engine's `TransitionAction` shape.
264
+ */
265
+ export interface LegacyProcessStep {
266
+ action: string;
267
+ target: LegacyCriteria;
268
+ waitAfter?: IRWaitSpec;
269
+ }
270
+
271
+ /**
272
+ * Legacy state-machine transition.
273
+ */
274
+ export interface LegacyTransition {
275
+ id: string;
276
+ name: string;
277
+ activateStates: string[];
278
+ deactivateStates: string[];
279
+ staysVisible: boolean;
280
+ process: LegacyProcessStep[];
281
+ }
282
+
283
+ /**
284
+ * Legacy state-machine state.
285
+ */
286
+ export interface LegacyStateMachineState {
287
+ id: string;
288
+ name: string;
289
+ description: string;
290
+ elements: LegacyCriteria[];
291
+ isInitial: boolean;
292
+ transitions: LegacyTransition[];
293
+ }
294
+
295
+ /**
296
+ * Legacy state-machine block.
297
+ */
298
+ export interface LegacyStateMachine {
299
+ states: LegacyStateMachineState[];
300
+ }
301
+
302
+ /**
303
+ * Legacy spec metadata block. The projection writes a deterministic subset;
304
+ * downstream tooling may layer additional keys on hand-authored specs.
305
+ */
306
+ export interface LegacyMetadata {
307
+ component: string;
308
+ tags?: string[];
309
+ [key: string]: unknown;
310
+ }
311
+
312
+ /**
313
+ * Top-level legacy spec shape.
314
+ */
315
+ export interface LegacySpec {
316
+ version: string;
317
+ description: string;
318
+ groups: LegacyGroup[];
319
+ stateMachine: LegacyStateMachine;
320
+ metadata: LegacyMetadata;
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Internal helpers
325
+ // ---------------------------------------------------------------------------
326
+
327
+ /**
328
+ * Recursively sort object keys lexicographically. Arrays preserve input order
329
+ * (ordering is meaningful for groups / assertions / transitions).
330
+ *
331
+ * Used as the final stage of every projected output to guarantee byte-stable
332
+ * JSON serialization downstream.
333
+ */
334
+ function sortKeys<T>(value: T): T {
335
+ if (Array.isArray(value)) {
336
+ return value.map(sortKeys) as unknown as T;
337
+ }
338
+ if (value !== null && typeof value === "object") {
339
+ const sorted: Record<string, unknown> = {};
340
+ const keys = Object.keys(value as Record<string, unknown>).sort();
341
+ for (const key of keys) {
342
+ sorted[key] = sortKeys((value as Record<string, unknown>)[key]);
343
+ }
344
+ return sorted as unknown as T;
345
+ }
346
+ return value;
347
+ }
348
+
349
+ /**
350
+ * Convert IR element criteria to legacy criteria. Renames `text` ->
351
+ * `textContent`, `attributes` -> `dataAttributes`. `accessibleName` (or
352
+ * `ariaLabel` as a fallback) maps to legacy `accessibleName`. `tagName`,
353
+ * `role`, `textContains`, `id` pass through unchanged.
354
+ */
355
+ function convertCriteria(criteria: IRElementCriteria): LegacyCriteria {
356
+ const out: LegacyCriteria = {};
357
+ if (criteria.role !== undefined) out.role = criteria.role;
358
+ if (criteria.tagName !== undefined) out.tagName = criteria.tagName;
359
+ if (criteria.text !== undefined) out.textContent = criteria.text;
360
+ if (criteria.textContains !== undefined) out.textContains = criteria.textContains;
361
+ // Prefer explicit `accessibleName` when present; fall back to `ariaLabel`
362
+ // for IR documents authored against the section-1 shape.
363
+ if (criteria.accessibleName !== undefined) {
364
+ out.accessibleName = criteria.accessibleName;
365
+ } else if (criteria.ariaLabel !== undefined) {
366
+ out.accessibleName = criteria.ariaLabel;
367
+ }
368
+ if (criteria.id !== undefined) out.id = criteria.id;
369
+ if (criteria.attributes !== undefined) out.dataAttributes = criteria.attributes;
370
+ return out;
371
+ }
372
+
373
+ /**
374
+ * Build the legacy `groups[]` entry for one IR state. With IrState.assertions
375
+ * carrying first-class assertion metadata (id/description/severity/category/
376
+ * target), this is now a direct shape-conversion per assertion — no more
377
+ * synthesizing IDs or defaulting severity.
378
+ */
379
+ function buildGroup(state: IRState): LegacyGroup {
380
+ const assertions: LegacyAssertion[] = state.assertions.map(synthesizedAssertionToLegacy);
381
+ return {
382
+ id: state.id,
383
+ name: state.name,
384
+ description: state.description ?? state.metadata?.description ?? "",
385
+ category: "element-presence",
386
+ assertions,
387
+ source: state.provenance?.source ?? "ai-generated",
388
+ };
389
+ }
390
+
391
+ /**
392
+ * Convert an `IRAssertion` (IR-side, free-form criteria) into a `LegacyAssertion`.
393
+ * The shapes are near-identical; the only meaningful difference is that the
394
+ * IR-side `target.criteria` is `Record<string, unknown>` while the legacy
395
+ * shape is `LegacyCriteria` (a typed but extensible record). We pass the
396
+ * criteria object through verbatim — the open `[key: string]: unknown` index
397
+ * signature on `LegacyCriteria` accepts any record shape.
398
+ */
399
+ function synthesizedAssertionToLegacy(assertion: IRAssertion): LegacyAssertion {
400
+ const target: LegacyAssertionTarget = {
401
+ type: "search",
402
+ criteria: assertion.target.criteria as LegacyCriteria,
403
+ label: assertion.target.label,
404
+ };
405
+ const out: LegacyAssertion = {
406
+ id: assertion.id,
407
+ description: assertion.description,
408
+ category: assertion.category,
409
+ severity: assertion.severity as LegacyAssertion["severity"],
410
+ assertionType: assertion.assertionType,
411
+ target,
412
+ source: assertion.source,
413
+ reviewed: assertion.reviewed,
414
+ enabled: assertion.enabled,
415
+ };
416
+ if (assertion.precondition !== undefined) {
417
+ out.precondition = assertion.precondition;
418
+ }
419
+ return out;
420
+ }
421
+
422
+ /**
423
+ * Convert a synthesized `IRGroup` into a `LegacyGroup`. Mirrors the legacy
424
+ * shape one-to-one (modulo the free-form criteria pass-through described in
425
+ * `synthesizedAssertionToLegacy`). The synthesized group always carries a
426
+ * `source` (synthesis emits `"ai-generated"`); fall back to `"ai-generated"`
427
+ * for hand-written IR documents that omit it.
428
+ */
429
+ function synthesizedGroupToLegacy(group: IRGroup): LegacyGroup {
430
+ return {
431
+ id: group.id,
432
+ name: group.name,
433
+ description: group.description,
434
+ category: group.category,
435
+ assertions: group.assertions.map(synthesizedAssertionToLegacy),
436
+ source: group.source ?? "ai-generated",
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Convert an IR transition action into the legacy `process[]` step shape.
442
+ * Renames IR's `type` to legacy `action` (matches the runtime engine
443
+ * convention — same as the IR -> WorkflowConfig adapter does for runtime).
444
+ */
445
+ function buildProcessStep(action: IRTransitionAction): LegacyProcessStep {
446
+ const out: LegacyProcessStep = {
447
+ action: action.type,
448
+ target: convertCriteria(action.target),
449
+ };
450
+ if (action.waitAfter !== undefined) out.waitAfter = action.waitAfter;
451
+ return out;
452
+ }
453
+
454
+ /**
455
+ * Convert one IR transition into the legacy state-machine transition shape.
456
+ * Each fromState gets its own copy in the projected output (legacy specs put
457
+ * the transition under each originating state's `transitions[]`).
458
+ */
459
+ function buildTransition(transition: IRTransition): LegacyTransition {
460
+ const exitStates = transition.exitStates ?? [];
461
+ return {
462
+ id: transition.id,
463
+ name: transition.name,
464
+ activateStates: transition.activateStates,
465
+ deactivateStates: exitStates,
466
+ // Modal-style transitions (no exit states) stay visible behind the
467
+ // activated state. Otherwise the originating state(s) deactivate, so the
468
+ // page no longer shows them.
469
+ staysVisible: exitStates.length === 0,
470
+ process: transition.actions.map(buildProcessStep),
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Build the legacy `stateMachine.states[]` entry for one IR state. Includes
476
+ * any transitions that originate from this state (i.e. where state.id is in
477
+ * transition.fromStates).
478
+ */
479
+ function buildStateMachineState(
480
+ state: IRState,
481
+ transitions: IRTransition[],
482
+ doc: IRDocument,
483
+ ): LegacyStateMachineState {
484
+ const outgoing = transitions
485
+ .filter((t) => t.fromStates.includes(state.id))
486
+ .map(buildTransition);
487
+ const isInitial =
488
+ state.isInitial !== undefined ? state.isInitial : state.id === doc.initialState;
489
+ return {
490
+ id: state.id,
491
+ name: state.name,
492
+ description: state.description ?? "",
493
+ // Project legacy `elements[]` from each assertion's target.criteria.
494
+ elements: state.assertions.map((a) =>
495
+ convertCriteria(a.target.criteria as IRElementCriteria),
496
+ ),
497
+ isInitial,
498
+ transitions: outgoing,
499
+ };
500
+ }
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // Public entry point
504
+ // ---------------------------------------------------------------------------
505
+
506
+ /**
507
+ * Project an IR document into the legacy bundled-page spec shape.
508
+ *
509
+ * Pure / deterministic: same input always produces structurally identical
510
+ * output (and byte-identical when JSON-serialized via the standard
511
+ * `JSON.stringify` ordering). Arrays preserve input order; object keys are
512
+ * sorted lexicographically.
513
+ *
514
+ * @param doc The IR document to project.
515
+ * @param notes Optional human-authored notes (carried in the page's
516
+ * `notes.md` companion file). Appended to `description` as a
517
+ * separate paragraph (separator: two newlines).
518
+ */
519
+ export function projectIRToBundledPage(doc: IRDocument, notes?: string): LegacySpec {
520
+ const baseDescription = doc.description ?? doc.name;
521
+ const description =
522
+ notes !== undefined && notes.length > 0
523
+ ? `${baseDescription}\n\n${notes}`
524
+ : baseDescription;
525
+
526
+ const component = doc.metadata?.purpose ?? doc.id;
527
+ const metadata: LegacyMetadata = { component };
528
+ if (doc.metadata?.tags !== undefined) metadata.tags = doc.metadata.tags;
529
+
530
+ const groups: LegacyGroup[] = doc.states.map(buildGroup);
531
+ if (doc.synthesizedGroups !== undefined) {
532
+ // Append synthesis-produced groups AFTER state-derived ones, in declared
533
+ // order. Byte-stable output (see file header) requires this exact ordering
534
+ // — anything else breaks `check-spec-pairing` style hashes.
535
+ for (const g of doc.synthesizedGroups) {
536
+ groups.push(synthesizedGroupToLegacy(g));
537
+ }
538
+ }
539
+ const smStates: LegacyStateMachineState[] = doc.states.map((s) =>
540
+ buildStateMachineState(s, doc.transitions, doc),
541
+ );
542
+
543
+ const spec: LegacySpec = {
544
+ version: "1.0.0",
545
+ description,
546
+ groups,
547
+ stateMachine: { states: smStates },
548
+ metadata,
549
+ };
550
+
551
+ return sortKeys(spec);
552
+ }
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // Inverse projection — Legacy spec -> IR document
556
+ // ---------------------------------------------------------------------------
557
+
558
+ /**
559
+ * Options for the inverse projection.
560
+ *
561
+ * - `docId` — explicit IRDocument.id override. Falls back to
562
+ * `legacy.metadata.component` (or `fallbackName` if absent).
563
+ * - `fallbackName` — used when `legacy.metadata.component` is missing AND no
564
+ * `docId` was supplied.
565
+ */
566
+ export interface ProjectLegacyToIROptions {
567
+ docId?: string;
568
+ fallbackName?: string;
569
+ }
570
+
571
+ /**
572
+ * Best-effort field-by-field invert of `LegacyCriteria` back into
573
+ * `IRElementCriteria`.
574
+ *
575
+ * Rename map (the structural inverse of `convertCriteria`):
576
+ * textContent -> text
577
+ * accessibleName -> accessibleName (passes through; the forward direction
578
+ * collapses ariaLabel into accessibleName,
579
+ * so we cannot reliably distinguish them
580
+ * on the inverse — pick accessibleName).
581
+ * dataAttributes -> attributes
582
+ * role / tagName / textContains / id pass through unchanged.
583
+ *
584
+ * Unknown legacy keys are dropped (lossy, but documented). Document-level
585
+ * unknown criteria are rare; if they show up we'll surface them in Phase A4.
586
+ */
587
+ function invertCriteria(criteria: LegacyCriteria | undefined): IRElementCriteria {
588
+ if (criteria === undefined || criteria === null) return {};
589
+ const out: IRElementCriteria = {};
590
+ if (criteria.role !== undefined) out.role = criteria.role;
591
+ if (criteria.tagName !== undefined) out.tagName = criteria.tagName;
592
+ if (criteria.textContent !== undefined) out.text = criteria.textContent;
593
+ if (criteria.textContains !== undefined) out.textContains = criteria.textContains;
594
+ if (criteria.accessibleName !== undefined) out.accessibleName = criteria.accessibleName;
595
+ if (criteria.id !== undefined) out.id = criteria.id;
596
+ if (criteria.dataAttributes !== undefined) out.attributes = criteria.dataAttributes;
597
+ return out;
598
+ }
599
+
600
+ /**
601
+ * Pick the first non-empty `precondition` across a group's assertions. Legacy
602
+ * groups carry preconditions on individual assertions; IR carries it on the
603
+ * state. If multiple distinct preconditions exist we keep the first
604
+ * (deterministic, mirrors input ordering) — Phase A4 review will surface
605
+ * conflicts for hand-fix.
606
+ */
607
+ function firstPrecondition(group: LegacyGroup): string | undefined {
608
+ for (const a of group.assertions) {
609
+ if (a.precondition !== undefined && a.precondition.length > 0) {
610
+ return a.precondition;
611
+ }
612
+ }
613
+ return undefined;
614
+ }
615
+
616
+ /**
617
+ * Build an `IRState` from one legacy group + its corresponding state-machine
618
+ * state (when present).
619
+ *
620
+ * IMPORTANT: `requiredElements` is sourced from the GROUP's assertion criteria
621
+ * (one criteria per assertion, in input order), NOT from `smState.elements`.
622
+ * This preserves the round-trip identity for the assertion-count check —
623
+ * legacy specs sometimes carry an SM block whose `elements[]` is a coarse
624
+ * page-state fingerprint (1-2 entries) while the group has many assertions.
625
+ * Using `smState.elements` would silently collapse those into one
626
+ * requiredElement per state, then the forward direction would emit only one
627
+ * assertion per group.
628
+ *
629
+ * `smState` (when present and matched by id) contributes:
630
+ * - `name` override (legacy `smState.name` is human-readable; group.name is
631
+ * also human-readable but may differ — prefer smState's name since the
632
+ * forward direction's `buildStateMachineState` echoes `state.name` to
633
+ * `smState.name`).
634
+ * - `description` fallback (when group.description is empty).
635
+ * - `isInitial` (the only place this lives in legacy specs).
636
+ */
637
+ function buildIRState(
638
+ group: LegacyGroup,
639
+ smState: LegacyStateMachineState | undefined,
640
+ ): IRState {
641
+ // Map each LegacyAssertion to an IRAssertion, preserving id/description/
642
+ // severity/category. target.criteria is normalized via invertCriteria so
643
+ // the criteria shape matches what the IR side stores.
644
+ const assertions: IRAssertion[] = group.assertions.map((a) => {
645
+ const ir: IRAssertion = {
646
+ id: a.id,
647
+ description: a.description,
648
+ category: a.category,
649
+ severity: a.severity,
650
+ assertionType: a.assertionType,
651
+ target: {
652
+ type: "search",
653
+ criteria: invertCriteria(a.target?.criteria) as unknown as Record<string, unknown>,
654
+ label: a.target?.label ?? a.description,
655
+ },
656
+ source: a.source,
657
+ reviewed: a.reviewed,
658
+ enabled: a.enabled,
659
+ };
660
+ if (a.precondition !== undefined) {
661
+ ir.precondition = a.precondition;
662
+ }
663
+ return ir;
664
+ });
665
+
666
+ const description =
667
+ group.description !== undefined && group.description.length > 0
668
+ ? group.description
669
+ : smState?.description;
670
+ const precondition = firstPrecondition(group);
671
+
672
+ const state: IRState = {
673
+ id: group.id,
674
+ name: smState?.name ?? group.name,
675
+ assertions,
676
+ };
677
+ if (description !== undefined && description.length > 0) {
678
+ state.description = description;
679
+ }
680
+ if (smState?.isInitial === true) {
681
+ state.isInitial = true;
682
+ } else if (smState?.isInitial === false) {
683
+ state.isInitial = false;
684
+ }
685
+ if (precondition !== undefined) {
686
+ state.precondition = precondition;
687
+ }
688
+ state.provenance = { source: "migrated" };
689
+ return state;
690
+ }
691
+
692
+ /**
693
+ * Convert one legacy `process[]` step back into `IRTransitionAction`. Renames
694
+ * `action` -> `type`. Legacy specs occasionally omit a target on no-op steps;
695
+ * we coerce to an empty criteria object in that case.
696
+ */
697
+ function invertProcessStep(step: LegacyProcessStep): IRTransitionAction {
698
+ const out: IRTransitionAction = {
699
+ type: step.action,
700
+ target: invertCriteria(step.target),
701
+ };
702
+ if (step.waitAfter !== undefined) out.waitAfter = step.waitAfter;
703
+ return out;
704
+ }
705
+
706
+ /**
707
+ * Collapse duplicate transitions across multiple originating states. The
708
+ * forward projection emits the same transition under EACH `fromState`'s
709
+ * `transitions[]` entry; the inverse must merge them back into ONE
710
+ * `IRTransition` whose `fromStates` lists every originator.
711
+ *
712
+ * Same-id transitions are assumed to be byte-identical except for the
713
+ * (implicit) originating state. If they differ in `process[]` /
714
+ * `activateStates` / `deactivateStates`, we keep the first occurrence and
715
+ * append the additional fromState. (Genuine divergence here is a defect to
716
+ * surface in Phase A4 review.)
717
+ */
718
+ function buildIRTransitions(stateMachine: LegacyStateMachine | undefined): IRTransition[] {
719
+ if (stateMachine === undefined || !Array.isArray(stateMachine.states)) return [];
720
+ const byId = new Map<string, IRTransition>();
721
+ // Preserve insertion order — first state's transitions come first.
722
+ const order: string[] = [];
723
+
724
+ for (const smState of stateMachine.states) {
725
+ const transitions = Array.isArray(smState.transitions) ? smState.transitions : [];
726
+ for (const t of transitions) {
727
+ const existing = byId.get(t.id);
728
+ if (existing !== undefined) {
729
+ if (!existing.fromStates.includes(smState.id)) {
730
+ existing.fromStates.push(smState.id);
731
+ }
732
+ continue;
733
+ }
734
+ const ir: IRTransition = {
735
+ id: t.id,
736
+ name: t.name,
737
+ fromStates: [smState.id],
738
+ activateStates: Array.isArray(t.activateStates) ? [...t.activateStates] : [],
739
+ actions: Array.isArray(t.process) ? t.process.map(invertProcessStep) : [],
740
+ };
741
+ // The forward direction maps `exitStates` -> `deactivateStates`; preserve
742
+ // the field whenever it's present in legacy form (even if empty array)
743
+ // so modal-style transitions (staysVisible: true, deactivateStates: [])
744
+ // round-trip predictably.
745
+ if (Array.isArray(t.deactivateStates)) {
746
+ ir.exitStates = [...t.deactivateStates];
747
+ }
748
+ ir.provenance = { source: "migrated" };
749
+ byId.set(t.id, ir);
750
+ order.push(t.id);
751
+ }
752
+ }
753
+
754
+ return order.map((id) => byId.get(id)!);
755
+ }
756
+
757
+ /**
758
+ * Project a legacy bundled-page spec back into IR.
759
+ *
760
+ * Pure / deterministic: same input always produces structurally identical
761
+ * output. Collapses transition duplicates (forward emits one copy per
762
+ * `fromState`), preserves group/assertion ordering, and tags every node with
763
+ * `provenance.source = "migrated"` so downstream tooling can distinguish
764
+ * migrated content from hand-authored.
765
+ *
766
+ * The mapping is the structural inverse of `projectIRToBundledPage`:
767
+ *
768
+ * LegacySpec IRDocument
769
+ * ---------- ----------
770
+ * metadata.component -> id (or opts.docId override)
771
+ * metadata.tags -> metadata.tags
772
+ * description -> description (kept verbatim;
773
+ * authored notes appended via
774
+ * forward `notes` arg are not
775
+ * stripped — that round-trip is
776
+ * best-effort and Phase A4 will
777
+ * split them back out)
778
+ *
779
+ * groups[] -> states[] (one per group)
780
+ * group.id / name / description -> state.id / name / description
781
+ * assertion.target.criteria -> state.requiredElements[i] (or,
782
+ * preferred, smState.elements when
783
+ * the stateMachine block is present)
784
+ * first-found assertion.precondition -> state.precondition
785
+ *
786
+ * stateMachine.states[] -> contributes isInitial + elements
787
+ * smState.transitions[] -> transitions[] (deduped by id)
788
+ * transition.activateStates -> transition.activateStates
789
+ * transition.deactivateStates -> transition.exitStates
790
+ * transition.process[] -> transition.actions[]
791
+ * step.action -> action.type
792
+ * step.target -> action.target (criteria inverted)
793
+ * step.waitAfter -> action.waitAfter
794
+ *
795
+ * Documented losses (acceptable for Phase A2):
796
+ * - Legacy assertion ids (`{stateId}-elem-{i}`) are NOT preserved across the
797
+ * round-trip — the forward direction regenerates them from index, so any
798
+ * hand-edited ids are lost. See Phase A4.
799
+ * - Legacy `assertionType` / `severity` / `category` / `source` per
800
+ * assertion are reduced to defaults on round-trip.
801
+ * - Legacy `expected` / `assertionType: "count"` and `notExists` semantics
802
+ * are not modeled in IR — the assertion's criteria still survive (as a
803
+ * state requiredElement) but the assertion-flavor metadata is dropped.
804
+ * - Legacy `testing` block + extra metadata keys outside `component` /
805
+ * `tags` are ignored. Authors keep them via the legacy spec file directly
806
+ * during Phase A2 — they will move into IR companion files in Phase A4.
807
+ * - The forward `notes` argument's appended paragraph is currently kept
808
+ * in `description`; Phase A4 will split it back out into a dedicated
809
+ * companion file.
810
+ *
811
+ * @param legacy The legacy spec to invert.
812
+ * @param opts Optional doc-level overrides.
813
+ */
814
+ export function projectLegacyToIR(
815
+ legacy: LegacySpec,
816
+ opts?: ProjectLegacyToIROptions,
817
+ ): IRDocument {
818
+ const component =
819
+ opts?.docId ??
820
+ legacy.metadata?.component ??
821
+ opts?.fallbackName ??
822
+ "legacy-spec";
823
+ const name = legacy.metadata?.component ?? opts?.fallbackName ?? component;
824
+
825
+ // Index state-machine states by id for quick lookup when building IR states
826
+ // out of groups. Older specs may not have a stateMachine block.
827
+ const smByGroupId = new Map<string, LegacyStateMachineState>();
828
+ if (legacy.stateMachine !== undefined && Array.isArray(legacy.stateMachine.states)) {
829
+ for (const s of legacy.stateMachine.states) {
830
+ smByGroupId.set(s.id, s);
831
+ }
832
+ }
833
+
834
+ const groups = Array.isArray(legacy.groups) ? legacy.groups : [];
835
+ const states: IRState[] = groups.map((group) =>
836
+ buildIRState(group, smByGroupId.get(group.id)),
837
+ );
838
+
839
+ // NOTE: Some legacy specs declare `stateMachine.states[]` whose ids do NOT
840
+ // correspond to any `groups[]` entry (e.g., page-level states like
841
+ // `wrappers-installed-tab` vs assertion-category groups like
842
+ // `page-structure`). The IR's group-state mapping is 1:1 with groups by
843
+ // construction (the forward projection emits one group per IR state), so
844
+ // these orphan SM states cannot survive the round-trip cleanly. They are
845
+ // dropped here as a documented loss — Phase A4 will revisit by either
846
+ // (a) introducing a separate IR concept for "page state" vs
847
+ // "assertion group", or (b) hand-merging the legacy specs so groups and
848
+ // SM states share ids.
849
+ // Transitions originating from orphan SM states are likewise omitted from
850
+ // IR (their fromStates would dangle). We filter them below.
851
+ const groupIds = new Set(groups.map((g) => g.id));
852
+
853
+ // Build transitions only from SM states whose ids appear in groups[]. Any
854
+ // transition with a fromState that isn't a known group is dropped — the
855
+ // forward direction would have nowhere to emit it.
856
+ let filteredStateMachine: LegacyStateMachine | undefined = legacy.stateMachine;
857
+ if (
858
+ legacy.stateMachine !== undefined &&
859
+ Array.isArray(legacy.stateMachine.states) &&
860
+ legacy.stateMachine.states.some((s) => !groupIds.has(s.id))
861
+ ) {
862
+ filteredStateMachine = {
863
+ states: legacy.stateMachine.states.filter((s) => groupIds.has(s.id)),
864
+ };
865
+ }
866
+ const transitions = buildIRTransitions(filteredStateMachine);
867
+
868
+ // Initial state: first state with isInitial:true wins (mirrors how the
869
+ // forward direction picks the document's `initialState`). Only consider
870
+ // SM states that correspond to a group (so initialState references a real
871
+ // IR state).
872
+ let initialState: string | undefined;
873
+ if (legacy.stateMachine !== undefined && Array.isArray(legacy.stateMachine.states)) {
874
+ const initial = legacy.stateMachine.states.find(
875
+ (s) => s.isInitial === true && groupIds.has(s.id),
876
+ );
877
+ if (initial !== undefined) initialState = initial.id;
878
+ }
879
+
880
+ const doc: IRDocument = {
881
+ version: "1.0",
882
+ id: component,
883
+ name,
884
+ states,
885
+ transitions,
886
+ provenance: { source: "migrated" },
887
+ };
888
+ if (legacy.description !== undefined && legacy.description.length > 0) {
889
+ doc.description = legacy.description;
890
+ }
891
+ if (legacy.metadata !== undefined && legacy.metadata.tags !== undefined) {
892
+ doc.metadata = { tags: legacy.metadata.tags };
893
+ }
894
+ if (initialState !== undefined) {
895
+ doc.initialState = initialState;
896
+ }
897
+ return doc;
898
+ }