@sun-asterisk/sungen 3.3.0-alpha.1 → 3.3.0-alpha.10

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 (463) hide show
  1. package/dist/capabilities/discover.d.ts +4 -0
  2. package/dist/capabilities/discover.d.ts.map +1 -1
  3. package/dist/capabilities/discover.js +16 -2
  4. package/dist/capabilities/discover.js.map +1 -1
  5. package/dist/cli/case-triage-hint.d.ts +14 -0
  6. package/dist/cli/case-triage-hint.d.ts.map +1 -0
  7. package/dist/cli/case-triage-hint.js +23 -0
  8. package/dist/cli/case-triage-hint.js.map +1 -0
  9. package/dist/cli/commands/audit.js +1 -1
  10. package/dist/cli/commands/audit.js.map +1 -1
  11. package/dist/cli/commands/contract-derive-command.d.ts +4 -2
  12. package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
  13. package/dist/cli/commands/contract-derive-command.js +19 -5
  14. package/dist/cli/commands/contract-derive-command.js.map +1 -1
  15. package/dist/cli/commands/contract.d.ts +3 -0
  16. package/dist/cli/commands/contract.d.ts.map +1 -1
  17. package/dist/cli/commands/contract.js +40 -7
  18. package/dist/cli/commands/contract.js.map +1 -1
  19. package/dist/cli/commands/delivery.d.ts.map +1 -1
  20. package/dist/cli/commands/delivery.js +59 -18
  21. package/dist/cli/commands/delivery.js.map +1 -1
  22. package/dist/cli/commands/gate.d.ts.map +1 -1
  23. package/dist/cli/commands/gate.js +2 -1
  24. package/dist/cli/commands/gate.js.map +1 -1
  25. package/dist/cli/commands/generate.d.ts.map +1 -1
  26. package/dist/cli/commands/generate.js +14 -2
  27. package/dist/cli/commands/generate.js.map +1 -1
  28. package/dist/cli/commands/journey.d.ts.map +1 -1
  29. package/dist/cli/commands/journey.js +6 -4
  30. package/dist/cli/commands/journey.js.map +1 -1
  31. package/dist/cli/commands/next.d.ts +7 -2
  32. package/dist/cli/commands/next.d.ts.map +1 -1
  33. package/dist/cli/commands/next.js +41 -10
  34. package/dist/cli/commands/next.js.map +1 -1
  35. package/dist/cli/commands/triage.d.ts +2 -1
  36. package/dist/cli/commands/triage.d.ts.map +1 -1
  37. package/dist/cli/commands/triage.js +45 -3
  38. package/dist/cli/commands/triage.js.map +1 -1
  39. package/dist/cli/commands/update.d.ts.map +1 -1
  40. package/dist/cli/commands/update.js +6 -2
  41. package/dist/cli/commands/update.js.map +1 -1
  42. package/dist/cli/index.js +5 -1
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/json-error-on-exit.d.ts +34 -0
  45. package/dist/cli/json-error-on-exit.d.ts.map +1 -0
  46. package/dist/cli/json-error-on-exit.js +63 -0
  47. package/dist/cli/json-error-on-exit.js.map +1 -0
  48. package/dist/cli/project-sungen-invocation.d.ts +13 -0
  49. package/dist/cli/project-sungen-invocation.d.ts.map +1 -0
  50. package/dist/cli/project-sungen-invocation.js +95 -0
  51. package/dist/cli/project-sungen-invocation.js.map +1 -0
  52. package/dist/cli/run-command-hints.d.ts +9 -0
  53. package/dist/cli/run-command-hints.d.ts.map +1 -0
  54. package/dist/cli/run-command-hints.js +84 -0
  55. package/dist/cli/run-command-hints.js.map +1 -0
  56. package/dist/cli/waiting-for-you-block.d.ts +18 -0
  57. package/dist/cli/waiting-for-you-block.d.ts.map +1 -0
  58. package/dist/cli/waiting-for-you-block.js +22 -0
  59. package/dist/cli/waiting-for-you-block.js.map +1 -0
  60. package/dist/contract/accept-opaque-steps.d.ts +3 -0
  61. package/dist/contract/accept-opaque-steps.d.ts.map +1 -0
  62. package/dist/contract/accept-opaque-steps.js +114 -0
  63. package/dist/contract/accept-opaque-steps.js.map +1 -0
  64. package/dist/contract/approve-contract.d.ts +8 -1
  65. package/dist/contract/approve-contract.d.ts.map +1 -1
  66. package/dist/contract/approve-contract.js +6 -0
  67. package/dist/contract/approve-contract.js.map +1 -1
  68. package/dist/contract/build-contract-verdict.js +2 -2
  69. package/dist/contract/build-contract-verdict.js.map +1 -1
  70. package/dist/contract/build-trace-conformance.d.ts +5 -1
  71. package/dist/contract/build-trace-conformance.d.ts.map +1 -1
  72. package/dist/contract/build-trace-conformance.js +12 -14
  73. package/dist/contract/build-trace-conformance.js.map +1 -1
  74. package/dist/contract/classify-contract-change.d.ts +3 -2
  75. package/dist/contract/classify-contract-change.d.ts.map +1 -1
  76. package/dist/contract/classify-contract-change.js.map +1 -1
  77. package/dist/contract/classify-trace-conformance.d.ts +18 -3
  78. package/dist/contract/classify-trace-conformance.d.ts.map +1 -1
  79. package/dist/contract/classify-trace-conformance.js +64 -17
  80. package/dist/contract/classify-trace-conformance.js.map +1 -1
  81. package/dist/contract/contract-schema.d.ts +21 -0
  82. package/dist/contract/contract-schema.d.ts.map +1 -1
  83. package/dist/contract/contract-schema.js +25 -3
  84. package/dist/contract/contract-schema.js.map +1 -1
  85. package/dist/contract/contracts-lock-file.d.ts.map +1 -1
  86. package/dist/contract/contracts-lock-file.js +1 -0
  87. package/dist/contract/contracts-lock-file.js.map +1 -1
  88. package/dist/contract/derive-free-feature.d.ts +19 -0
  89. package/dist/contract/derive-free-feature.d.ts.map +1 -1
  90. package/dist/contract/derive-free-feature.js +40 -2
  91. package/dist/contract/derive-free-feature.js.map +1 -1
  92. package/dist/contract/project-contract-blockers.d.ts.map +1 -1
  93. package/dist/contract/project-contract-blockers.js +4 -2
  94. package/dist/contract/project-contract-blockers.js.map +1 -1
  95. package/dist/contract/record-contract-approval.d.ts +16 -1
  96. package/dist/contract/record-contract-approval.d.ts.map +1 -1
  97. package/dist/contract/record-contract-approval.js +29 -1
  98. package/dist/contract/record-contract-approval.js.map +1 -1
  99. package/dist/contract/render-free-gherkin.d.ts +2 -0
  100. package/dist/contract/render-free-gherkin.d.ts.map +1 -1
  101. package/dist/contract/render-free-gherkin.js +6 -0
  102. package/dist/contract/render-free-gherkin.js.map +1 -1
  103. package/dist/contract/resolve-unit-trace-identities.d.ts +12 -0
  104. package/dist/contract/resolve-unit-trace-identities.d.ts.map +1 -1
  105. package/dist/contract/resolve-unit-trace-identities.js +33 -49
  106. package/dist/contract/resolve-unit-trace-identities.js.map +1 -1
  107. package/dist/contract/trace/collect-trace-evidence.d.ts +16 -5
  108. package/dist/contract/trace/collect-trace-evidence.d.ts.map +1 -1
  109. package/dist/contract/trace/collect-trace-evidence.js +101 -43
  110. package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
  111. package/dist/contract/trace/read-file-once.d.ts +20 -0
  112. package/dist/contract/trace/read-file-once.d.ts.map +1 -0
  113. package/dist/contract/trace/read-file-once.js +135 -0
  114. package/dist/contract/trace/read-file-once.js.map +1 -0
  115. package/dist/contract/trace/scenario-id-claims.d.ts +11 -0
  116. package/dist/contract/trace/scenario-id-claims.d.ts.map +1 -0
  117. package/dist/contract/trace/scenario-id-claims.js +89 -0
  118. package/dist/contract/trace/scenario-id-claims.js.map +1 -0
  119. package/dist/contract/trace/select-final-attempt.d.ts +4 -2
  120. package/dist/contract/trace/select-final-attempt.d.ts.map +1 -1
  121. package/dist/contract/trace/select-final-attempt.js +7 -4
  122. package/dist/contract/trace/select-final-attempt.js.map +1 -1
  123. package/dist/contract/trace/test-step-join.d.ts +3 -3
  124. package/dist/contract/trace/test-step-join.d.ts.map +1 -1
  125. package/dist/contract/trace/test-step-join.js +2 -2
  126. package/dist/contract/trace/test-step-join.js.map +1 -1
  127. package/dist/contract/trace/trace-binding.d.ts +3 -2
  128. package/dist/contract/trace/trace-binding.d.ts.map +1 -1
  129. package/dist/contract/trace/trace-binding.js +6 -3
  130. package/dist/contract/trace/trace-binding.js.map +1 -1
  131. package/dist/contract/trace/trace-file-reader.d.ts +14 -2
  132. package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
  133. package/dist/contract/trace/trace-file-reader.js +27 -45
  134. package/dist/contract/trace/trace-file-reader.js.map +1 -1
  135. package/dist/contract/trace/trace-freshness.d.ts +27 -0
  136. package/dist/contract/trace/trace-freshness.d.ts.map +1 -0
  137. package/dist/contract/trace/trace-freshness.js +146 -0
  138. package/dist/contract/trace/trace-freshness.js.map +1 -0
  139. package/dist/contract/trace/trace-identity-resolution.d.ts +2 -0
  140. package/dist/contract/trace/trace-identity-resolution.d.ts.map +1 -1
  141. package/dist/contract/trace/trace-identity-resolution.js +6 -3
  142. package/dist/contract/trace/trace-identity-resolution.js.map +1 -1
  143. package/dist/contract/trace/trace-run-record.d.ts +8 -3
  144. package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
  145. package/dist/contract/trace/trace-run-record.js +42 -22
  146. package/dist/contract/trace/trace-run-record.js.map +1 -1
  147. package/dist/contract/trace/trace-steps.js +1 -1
  148. package/dist/contract/trace/trace-steps.js.map +1 -1
  149. package/dist/contract/trace/trace-zip-limits.d.ts +5 -1
  150. package/dist/contract/trace/trace-zip-limits.d.ts.map +1 -1
  151. package/dist/contract/trace/trace-zip-limits.js +6 -2
  152. package/dist/contract/trace/trace-zip-limits.js.map +1 -1
  153. package/dist/contract/verify-approval-decisions.d.ts.map +1 -1
  154. package/dist/contract/verify-approval-decisions.js +9 -4
  155. package/dist/contract/verify-approval-decisions.js.map +1 -1
  156. package/dist/decisions/confirmed-decision-commands.d.ts +11 -0
  157. package/dist/decisions/confirmed-decision-commands.d.ts.map +1 -1
  158. package/dist/decisions/confirmed-decision-commands.js +21 -3
  159. package/dist/decisions/confirmed-decision-commands.js.map +1 -1
  160. package/dist/decisions/decision-schema.d.ts +2 -1
  161. package/dist/decisions/decision-schema.d.ts.map +1 -1
  162. package/dist/decisions/decision-schema.js +2 -1
  163. package/dist/decisions/decision-schema.js.map +1 -1
  164. package/dist/decisions/git-head-trust.d.ts +12 -0
  165. package/dist/decisions/git-head-trust.d.ts.map +1 -1
  166. package/dist/decisions/git-head-trust.js +52 -0
  167. package/dist/decisions/git-head-trust.js.map +1 -1
  168. package/dist/decisions/human-confirmation.d.ts +2 -0
  169. package/dist/decisions/human-confirmation.d.ts.map +1 -1
  170. package/dist/decisions/human-confirmation.js +6 -0
  171. package/dist/decisions/human-confirmation.js.map +1 -1
  172. package/dist/decisions/legacy-waiver-migration.d.ts +8 -1
  173. package/dist/decisions/legacy-waiver-migration.d.ts.map +1 -1
  174. package/dist/decisions/legacy-waiver-migration.js +13 -4
  175. package/dist/decisions/legacy-waiver-migration.js.map +1 -1
  176. package/dist/exporters/csv-exporter.d.ts +11 -2
  177. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  178. package/dist/exporters/csv-exporter.js +8 -4
  179. package/dist/exporters/csv-exporter.js.map +1 -1
  180. package/dist/exporters/matrix/export.d.ts +6 -1
  181. package/dist/exporters/matrix/export.d.ts.map +1 -1
  182. package/dist/exporters/matrix/export.js +11 -4
  183. package/dist/exporters/matrix/export.js.map +1 -1
  184. package/dist/exporters/matrix/xlsx-cells.js +1 -1
  185. package/dist/exporters/matrix/xlsx-cells.js.map +1 -1
  186. package/dist/exporters/xlsx-exporter.d.ts +3 -2
  187. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  188. package/dist/exporters/xlsx-exporter.js +4 -4
  189. package/dist/exporters/xlsx-exporter.js.map +1 -1
  190. package/dist/graph/board-human-actions.d.ts +5 -0
  191. package/dist/graph/board-human-actions.d.ts.map +1 -0
  192. package/dist/graph/board-human-actions.js +83 -0
  193. package/dist/graph/board-human-actions.js.map +1 -0
  194. package/dist/graph/evaluate-graph.d.ts +21 -1
  195. package/dist/graph/evaluate-graph.d.ts.map +1 -1
  196. package/dist/graph/evaluate-graph.js +37 -9
  197. package/dist/graph/evaluate-graph.js.map +1 -1
  198. package/dist/graph/human-action.d.ts +21 -0
  199. package/dist/graph/human-action.d.ts.map +1 -0
  200. package/dist/graph/human-action.js +3 -0
  201. package/dist/graph/human-action.js.map +1 -0
  202. package/dist/graph/manual-triage-decisions.d.ts +19 -0
  203. package/dist/graph/manual-triage-decisions.d.ts.map +1 -0
  204. package/dist/graph/manual-triage-decisions.js +53 -0
  205. package/dist/graph/manual-triage-decisions.js.map +1 -0
  206. package/dist/graph/node-decl.d.ts +16 -0
  207. package/dist/graph/node-decl.d.ts.map +1 -1
  208. package/dist/graph/node-decl.js +4 -1
  209. package/dist/graph/node-decl.js.map +1 -1
  210. package/dist/graph/nodes/test-case-contract-nodes.d.ts +3 -1
  211. package/dist/graph/nodes/test-case-contract-nodes.d.ts.map +1 -1
  212. package/dist/graph/nodes/test-case-contract-nodes.js +22 -6
  213. package/dist/graph/nodes/test-case-contract-nodes.js.map +1 -1
  214. package/dist/graph/nodes/test-case-nodes.d.ts +3 -0
  215. package/dist/graph/nodes/test-case-nodes.d.ts.map +1 -1
  216. package/dist/graph/nodes/test-case-nodes.js +33 -4
  217. package/dist/graph/nodes/test-case-nodes.js.map +1 -1
  218. package/dist/graph/nodes/unit-audit-nodes.d.ts.map +1 -1
  219. package/dist/graph/nodes/unit-audit-nodes.js +1 -0
  220. package/dist/graph/nodes/unit-audit-nodes.js.map +1 -1
  221. package/dist/graph/nodes/unit-contract-node.d.ts +2 -1
  222. package/dist/graph/nodes/unit-contract-node.d.ts.map +1 -1
  223. package/dist/graph/nodes/unit-contract-node.js +16 -1
  224. package/dist/graph/nodes/unit-contract-node.js.map +1 -1
  225. package/dist/graph/nodes/unit-design-nodes.d.ts.map +1 -1
  226. package/dist/graph/nodes/unit-design-nodes.js +2 -1
  227. package/dist/graph/nodes/unit-design-nodes.js.map +1 -1
  228. package/dist/graph/test-case-results.d.ts +1 -0
  229. package/dist/graph/test-case-results.d.ts.map +1 -1
  230. package/dist/graph/test-case-results.js +27 -3
  231. package/dist/graph/test-case-results.js.map +1 -1
  232. package/dist/graph/test-case-satisfied.d.ts +5 -4
  233. package/dist/graph/test-case-satisfied.d.ts.map +1 -1
  234. package/dist/graph/test-case-satisfied.js +4 -0
  235. package/dist/graph/test-case-satisfied.js.map +1 -1
  236. package/dist/graph/test-case-state-shape.d.ts +7 -1
  237. package/dist/graph/test-case-state-shape.d.ts.map +1 -1
  238. package/dist/graph/unit-artifact-probes.d.ts.map +1 -1
  239. package/dist/graph/unit-artifact-probes.js +3 -1
  240. package/dist/graph/unit-artifact-probes.js.map +1 -1
  241. package/dist/graph/unit-results-freshness.d.ts +29 -0
  242. package/dist/graph/unit-results-freshness.d.ts.map +1 -0
  243. package/dist/graph/unit-results-freshness.js +213 -0
  244. package/dist/graph/unit-results-freshness.js.map +1 -0
  245. package/dist/harness/audit.d.ts +3 -2
  246. package/dist/harness/audit.d.ts.map +1 -1
  247. package/dist/harness/audit.js +4 -17
  248. package/dist/harness/audit.js.map +1 -1
  249. package/dist/harness/catalog/gate-rules.yaml +3 -3
  250. package/dist/harness/defect-draft.d.ts.map +1 -1
  251. package/dist/harness/defect-draft.js +12 -2
  252. package/dist/harness/defect-draft.js.map +1 -1
  253. package/dist/harness/defects.d.ts +14 -0
  254. package/dist/harness/defects.d.ts.map +1 -1
  255. package/dist/harness/defects.js +14 -10
  256. package/dist/harness/defects.js.map +1 -1
  257. package/dist/harness/flow-check.d.ts +9 -0
  258. package/dist/harness/flow-check.d.ts.map +1 -1
  259. package/dist/harness/flow-check.js +11 -0
  260. package/dist/harness/flow-check.js.map +1 -1
  261. package/dist/harness/gates/audit-gate-verdict.d.ts.map +1 -1
  262. package/dist/harness/gates/audit-gate-verdict.js +2 -1
  263. package/dist/harness/gates/audit-gate-verdict.js.map +1 -1
  264. package/dist/harness/gates/audit-input-hash.d.ts +2 -0
  265. package/dist/harness/gates/audit-input-hash.d.ts.map +1 -1
  266. package/dist/harness/gates/audit-input-hash.js +30 -2
  267. package/dist/harness/gates/audit-input-hash.js.map +1 -1
  268. package/dist/harness/gates/audit-report-verification.d.ts +17 -6
  269. package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
  270. package/dist/harness/gates/audit-report-verification.js +55 -21
  271. package/dist/harness/gates/audit-report-verification.js.map +1 -1
  272. package/dist/harness/gates/delivery-checklist.d.ts +3 -0
  273. package/dist/harness/gates/delivery-checklist.d.ts.map +1 -1
  274. package/dist/harness/gates/delivery-checklist.js +39 -6
  275. package/dist/harness/gates/delivery-checklist.js.map +1 -1
  276. package/dist/harness/gates/delivery-decision-gate.d.ts +4 -0
  277. package/dist/harness/gates/delivery-decision-gate.d.ts.map +1 -1
  278. package/dist/harness/gates/delivery-decision-gate.js +26 -1
  279. package/dist/harness/gates/delivery-decision-gate.js.map +1 -1
  280. package/dist/harness/gates/delivery-decision-step.d.ts +4 -1
  281. package/dist/harness/gates/delivery-decision-step.d.ts.map +1 -1
  282. package/dist/harness/gates/delivery-decision-step.js +17 -2
  283. package/dist/harness/gates/delivery-decision-step.js.map +1 -1
  284. package/dist/harness/gates/in-run-decision-check.d.ts.map +1 -1
  285. package/dist/harness/gates/in-run-decision-check.js +40 -12
  286. package/dist/harness/gates/in-run-decision-check.js.map +1 -1
  287. package/dist/harness/gates/migrated-waiver-import-bounds.d.ts +13 -0
  288. package/dist/harness/gates/migrated-waiver-import-bounds.d.ts.map +1 -0
  289. package/dist/harness/gates/migrated-waiver-import-bounds.js +141 -0
  290. package/dist/harness/gates/migrated-waiver-import-bounds.js.map +1 -0
  291. package/dist/harness/gates/migrated-waiver-review.d.ts +14 -4
  292. package/dist/harness/gates/migrated-waiver-review.d.ts.map +1 -1
  293. package/dist/harness/gates/migrated-waiver-review.js +78 -80
  294. package/dist/harness/gates/migrated-waiver-review.js.map +1 -1
  295. package/dist/harness/journey.d.ts +40 -0
  296. package/dist/harness/journey.d.ts.map +1 -1
  297. package/dist/harness/journey.js +155 -22
  298. package/dist/harness/journey.js.map +1 -1
  299. package/dist/harness/next-step.d.ts +19 -0
  300. package/dist/harness/next-step.d.ts.map +1 -1
  301. package/dist/harness/next-step.js +21 -0
  302. package/dist/harness/next-step.js.map +1 -1
  303. package/dist/harness/provenance.d.ts +29 -0
  304. package/dist/harness/provenance.d.ts.map +1 -1
  305. package/dist/harness/provenance.js +109 -9
  306. package/dist/harness/provenance.js.map +1 -1
  307. package/dist/harness/triage-manual-classification.d.ts +32 -0
  308. package/dist/harness/triage-manual-classification.d.ts.map +1 -0
  309. package/dist/harness/triage-manual-classification.js +56 -0
  310. package/dist/harness/triage-manual-classification.js.map +1 -0
  311. package/dist/harness/unit-paths.d.ts +8 -0
  312. package/dist/harness/unit-paths.d.ts.map +1 -1
  313. package/dist/harness/unit-paths.js +23 -1
  314. package/dist/harness/unit-paths.js.map +1 -1
  315. package/dist/hooks/bash-read-only-segment.d.ts +9 -0
  316. package/dist/hooks/bash-read-only-segment.d.ts.map +1 -0
  317. package/dist/hooks/bash-read-only-segment.js +168 -0
  318. package/dist/hooks/bash-read-only-segment.js.map +1 -0
  319. package/dist/hooks/bash-write-targets.d.ts +12 -2
  320. package/dist/hooks/bash-write-targets.d.ts.map +1 -1
  321. package/dist/hooks/bash-write-targets.js +122 -29
  322. package/dist/hooks/bash-write-targets.js.map +1 -1
  323. package/dist/hooks/guard-protected-paths.d.ts +4 -0
  324. package/dist/hooks/guard-protected-paths.d.ts.map +1 -1
  325. package/dist/hooks/guard-protected-paths.js +13 -3
  326. package/dist/hooks/guard-protected-paths.js.map +1 -1
  327. package/dist/hooks/guard-rules.d.ts +0 -17
  328. package/dist/hooks/guard-rules.d.ts.map +1 -1
  329. package/dist/hooks/guard-rules.js +79 -10
  330. package/dist/hooks/guard-rules.js.map +1 -1
  331. package/dist/hooks/guard-tty-spoof-rule.d.ts +3 -1
  332. package/dist/hooks/guard-tty-spoof-rule.d.ts.map +1 -1
  333. package/dist/hooks/guard-tty-spoof-rule.js +3 -2
  334. package/dist/hooks/guard-tty-spoof-rule.js.map +1 -1
  335. package/dist/hooks/hand-back-core.d.ts +9 -4
  336. package/dist/hooks/hand-back-core.d.ts.map +1 -1
  337. package/dist/hooks/hand-back-core.js +24 -11
  338. package/dist/hooks/hand-back-core.js.map +1 -1
  339. package/dist/hooks/session-start-unit-state-hook.d.ts +2 -1
  340. package/dist/hooks/session-start-unit-state-hook.d.ts.map +1 -1
  341. package/dist/hooks/session-start-unit-state-hook.js +9 -4
  342. package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
  343. package/dist/hooks/stop-next-step-hook.d.ts.map +1 -1
  344. package/dist/hooks/stop-next-step-hook.js +1 -0
  345. package/dist/hooks/stop-next-step-hook.js.map +1 -1
  346. package/dist/orchestrator/assets-drift.d.ts +5 -0
  347. package/dist/orchestrator/assets-drift.d.ts.map +1 -1
  348. package/dist/orchestrator/assets-drift.js +12 -3
  349. package/dist/orchestrator/assets-drift.js.map +1 -1
  350. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  351. package/dist/orchestrator/project-initializer.js +1 -0
  352. package/dist/orchestrator/project-initializer.js.map +1 -1
  353. package/dist/orchestrator/templates/ai-src/commands/create-test.md +6 -3
  354. package/dist/orchestrator/templates/ai-src/commands/delivery.md +2 -0
  355. package/dist/orchestrator/templates/ai-src/commands/review.md +2 -0
  356. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  357. package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
  358. package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
  359. package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
  360. package/package.json +3 -3
  361. package/src/capabilities/discover.ts +15 -2
  362. package/src/cli/case-triage-hint.ts +31 -0
  363. package/src/cli/commands/audit.ts +1 -1
  364. package/src/cli/commands/contract-derive-command.ts +20 -8
  365. package/src/cli/commands/contract.ts +41 -7
  366. package/src/cli/commands/delivery.ts +56 -18
  367. package/src/cli/commands/gate.ts +2 -1
  368. package/src/cli/commands/generate.ts +13 -2
  369. package/src/cli/commands/journey.ts +8 -6
  370. package/src/cli/commands/next.ts +41 -12
  371. package/src/cli/commands/triage.ts +41 -3
  372. package/src/cli/commands/update.ts +6 -2
  373. package/src/cli/index.ts +5 -1
  374. package/src/cli/json-error-on-exit.ts +72 -0
  375. package/src/cli/project-sungen-invocation.ts +59 -0
  376. package/src/cli/run-command-hints.ts +47 -0
  377. package/src/cli/waiting-for-you-block.ts +27 -0
  378. package/src/contract/accept-opaque-steps.ts +114 -0
  379. package/src/contract/approve-contract.ts +10 -3
  380. package/src/contract/build-contract-verdict.ts +2 -2
  381. package/src/contract/build-trace-conformance.ts +11 -8
  382. package/src/contract/classify-contract-change.ts +3 -3
  383. package/src/contract/classify-trace-conformance.ts +76 -22
  384. package/src/contract/contract-schema.ts +31 -2
  385. package/src/contract/contracts-lock-file.ts +1 -0
  386. package/src/contract/derive-free-feature.ts +46 -3
  387. package/src/contract/project-contract-blockers.ts +4 -2
  388. package/src/contract/record-contract-approval.ts +41 -1
  389. package/src/contract/render-free-gherkin.ts +5 -0
  390. package/src/contract/resolve-unit-trace-identities.ts +35 -12
  391. package/src/contract/trace/collect-trace-evidence.ts +105 -36
  392. package/src/contract/trace/read-file-once.ts +95 -0
  393. package/src/contract/trace/scenario-id-claims.ts +60 -0
  394. package/src/contract/trace/select-final-attempt.ts +12 -5
  395. package/src/contract/trace/test-step-join.ts +6 -5
  396. package/src/contract/trace/trace-binding.ts +7 -4
  397. package/src/contract/trace/trace-file-reader.ts +34 -13
  398. package/src/contract/trace/trace-freshness.ts +110 -0
  399. package/src/contract/trace/trace-identity-resolution.ts +7 -4
  400. package/src/contract/trace/trace-run-record.ts +37 -15
  401. package/src/contract/trace/trace-steps.ts +1 -1
  402. package/src/contract/trace/trace-zip-limits.ts +6 -2
  403. package/src/contract/verify-approval-decisions.ts +9 -5
  404. package/src/decisions/confirmed-decision-commands.ts +25 -4
  405. package/src/decisions/decision-schema.ts +2 -1
  406. package/src/decisions/git-head-trust.ts +44 -0
  407. package/src/decisions/human-confirmation.ts +5 -0
  408. package/src/decisions/legacy-waiver-migration.ts +14 -3
  409. package/src/exporters/csv-exporter.ts +12 -4
  410. package/src/exporters/matrix/export.ts +11 -4
  411. package/src/exporters/matrix/xlsx-cells.ts +1 -1
  412. package/src/exporters/xlsx-exporter.ts +5 -4
  413. package/src/graph/board-human-actions.ts +80 -0
  414. package/src/graph/evaluate-graph.ts +45 -10
  415. package/src/graph/human-action.ts +33 -0
  416. package/src/graph/manual-triage-decisions.ts +55 -0
  417. package/src/graph/node-decl.ts +18 -0
  418. package/src/graph/nodes/test-case-contract-nodes.ts +28 -7
  419. package/src/graph/nodes/test-case-nodes.ts +37 -5
  420. package/src/graph/nodes/unit-audit-nodes.ts +2 -1
  421. package/src/graph/nodes/unit-contract-node.ts +20 -2
  422. package/src/graph/nodes/unit-design-nodes.ts +2 -1
  423. package/src/graph/test-case-results.ts +25 -4
  424. package/src/graph/test-case-satisfied.ts +7 -4
  425. package/src/graph/test-case-state-shape.ts +7 -2
  426. package/src/graph/unit-artifact-probes.ts +3 -1
  427. package/src/graph/unit-results-freshness.ts +165 -0
  428. package/src/harness/audit.ts +4 -13
  429. package/src/harness/catalog/gate-rules.yaml +3 -3
  430. package/src/harness/defect-draft.ts +13 -2
  431. package/src/harness/defects.ts +22 -9
  432. package/src/harness/flow-check.ts +11 -0
  433. package/src/harness/gates/audit-gate-verdict.ts +2 -1
  434. package/src/harness/gates/audit-input-hash.ts +27 -3
  435. package/src/harness/gates/audit-report-verification.ts +57 -24
  436. package/src/harness/gates/delivery-checklist.ts +39 -7
  437. package/src/harness/gates/delivery-decision-gate.ts +29 -1
  438. package/src/harness/gates/delivery-decision-step.ts +19 -3
  439. package/src/harness/gates/in-run-decision-check.ts +38 -13
  440. package/src/harness/gates/migrated-waiver-import-bounds.ts +103 -0
  441. package/src/harness/gates/migrated-waiver-review.ts +90 -37
  442. package/src/harness/journey.ts +174 -21
  443. package/src/harness/next-step.ts +34 -1
  444. package/src/harness/provenance.ts +99 -11
  445. package/src/harness/triage-manual-classification.ts +76 -0
  446. package/src/harness/unit-paths.ts +19 -0
  447. package/src/hooks/bash-read-only-segment.ts +130 -0
  448. package/src/hooks/bash-write-targets.ts +109 -23
  449. package/src/hooks/guard-protected-paths.ts +13 -3
  450. package/src/hooks/guard-rules.ts +37 -18
  451. package/src/hooks/guard-tty-spoof-rule.ts +6 -3
  452. package/src/hooks/hand-back-core.ts +24 -12
  453. package/src/hooks/session-start-unit-state-hook.ts +9 -4
  454. package/src/hooks/stop-next-step-hook.ts +1 -0
  455. package/src/orchestrator/assets-drift.ts +12 -5
  456. package/src/orchestrator/project-initializer.ts +1 -0
  457. package/src/orchestrator/templates/ai-src/commands/create-test.md +6 -3
  458. package/src/orchestrator/templates/ai-src/commands/delivery.md +2 -0
  459. package/src/orchestrator/templates/ai-src/commands/review.md +2 -0
  460. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  461. package/src/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
  462. package/src/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
  463. package/src/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
@@ -7,26 +7,43 @@
7
7
  * with that step — identically (fine), weaker (hard), stronger (strengthened), changed
8
8
  * (review); an approved check with no group or no check in its group → assertion-lost
9
9
  * overlays a step whose target identity is `low` → opaque (review, never match, red-team #4);
10
- * an opaque step → opaque with its verbatim text and file:line (contract.opaque-step)
10
+ * an opaque step → opaque with its verbatim text and file:line (contract.opaque-step) —
11
+ * unless the approved entry lists it as accepted by QA at its sha (#923, opaqueStepSha):
12
+ * then a note, not a finding. impl:code: a test.step whose only candidates are opaque is
13
+ * left to the overlay (judged on the ladder it could only read "changed"), which names the
14
+ * approved step the opaque one replaces
11
15
  * runs no admissible trace → unproven (all inconclusive → inconclusive) when APPROVED, a
12
16
  * warning while proposed (M5, M6); a trace refused beside an admissible one → a warning;
13
17
  * several admissible traces that fingerprint differently → nondeterministic (worst wins)
14
18
  */
15
19
  import { classifyScenario, ContractFinding, finding, ScenarioResult, severityOf } from './classify-contract-change';
16
- import { sameJson } from './contract-hash';
17
- import type { ContractStep, Implementation, LockEntry, ScenarioClass } from './contract-schema';
20
+ import { canonicalJson, sameJson, sha256Hex } from './contract-hash';
21
+ import type { AcceptedOpaqueStep, ContractStep, Implementation, LockEntry, OpaqueStep, ScenarioClass } from './contract-schema';
18
22
  import { compareAsserts } from './oracle-strength';
23
+ import { clip, RenderedStep } from './render-free-gherkin';
19
24
  import type { TraceRun } from './trace/collect-trace-evidence';
20
25
  import type { TraceFingerprint } from './trace/trace-fingerprint';
21
26
 
22
- /** `warnings` never change the verdict: a not-yet-approved scenario's trace problems, unused traces. */
23
- export interface TraceOutcome { class: ScenarioClass; findings: ContractFinding[]; hashes: string[]; runs: string[]; warnings: string[] }
27
+ /**
28
+ * `warnings` never change the verdict: a not-yet-approved scenario's trace problems, unused traces, an
29
+ * accepted opaque step. `opaque`: every opaque step of the admissible traces, accepted or not.
30
+ */
31
+ export interface TraceOutcome { class: ScenarioClass; findings: ContractFinding[]; hashes: string[]; runs: string[]; warnings: string[]; opaque: OpaqueStep[] }
24
32
 
25
33
  /** What an unjudgeable scenario becomes: hard when QA locked it (approved), a warning while it is proposed. */
26
34
  export function notJudged(id: string, approved: boolean, cls: 'unproven' | 'inconclusive', reasons: string[]): TraceOutcome {
27
35
  return approved
28
- ? { class: cls, findings: reasons.map((r) => finding(cls, r)), hashes: [], runs: [], warnings: [] }
29
- : { class: 'proposed', findings: [], hashes: [], runs: [], warnings: reasons.map((r) => `${id} (not approved yet, so a warning): ${r}`) };
36
+ ? { class: cls, findings: reasons.map((r) => finding(cls, r)), hashes: [], runs: [], warnings: [], opaque: [] }
37
+ : { class: 'proposed', findings: [], hashes: [], runs: [], warnings: reasons.map((r) => `${id} (not approved yet, so a warning): ${r}`), opaque: [] };
38
+ }
39
+
40
+ /**
41
+ * The sha a QA acceptance binds to (#923): the opaque step as rendered — its verbatim line or request,
42
+ * check or step, the `test.step` it sits in — and as normalized, with the implementation path. Any
43
+ * change to the step gives another sha, so the old acceptance no longer covers it.
44
+ */
45
+ export function opaqueStepSha(implementation: Implementation, r: RenderedStep, step: ContractStep | undefined): string {
46
+ return sha256Hex(canonicalJson({ implementation, kind: r.kind, group: r.group ?? null, verbatim: r.opaque?.verbatim ?? null, step: step ?? null })).slice(0, 16);
30
47
  }
31
48
 
32
49
  /** How a low target identity opens its finding — `next` reads it to offer `contract check --trace`. */
@@ -35,19 +52,41 @@ export const LOW_IDENTITY_FINDING = 'target identity is low';
35
52
  const worstOf = (findings: ContractFinding[], start: ScenarioClass = 'match'): ScenarioClass =>
36
53
  findings.reduce<ScenarioClass>((acc, f) => (severityOf(f.class as ScenarioClass) > severityOf(acc) ? f.class as ScenarioClass : acc), start);
37
54
 
38
- function overlays(fp: TraceFingerprint, used?: Set<number>): ContractFinding[] {
39
- const out: ContractFinding[] = [];
55
+ type Judged = { findings: ContractFinding[]; opaque: OpaqueStep[] };
56
+
57
+ /** An approved step as a reader sees it: `see [My Account] heading with text "My Account"`. */
58
+ function approvedStepText(s: ContractStep): string {
59
+ const refs = [s.target?.ref, ...(s.target?.also ?? [])].filter((r): r is string => r !== undefined);
60
+ const values = (s.kind === 'assert' ? s.expected : s.value)?.split(' | ') ?? [];
61
+ let r = 0;
62
+ let v = 0;
63
+ return s.verb.replace(/\[\]/g, () => `[${refs[r++] ?? ''}]`).replace(/\{\}/g, () => (v < values.length ? `"${values[v++]}"` : '{}'));
64
+ }
65
+
66
+ function overlays(
67
+ fp: TraceFingerprint, implementation: Implementation, accepted: readonly AcceptedOpaqueStep[], used?: Set<number>, replaces?: ReadonlyMap<number, string>,
68
+ ): Judged {
69
+ const findings: ContractFinding[] = [];
70
+ const opaque: OpaqueStep[] = [];
40
71
  fp.rendered.forEach((r, i) => {
41
72
  if (used && !used.has(i)) return;
42
- if (r.opaque) out.push(finding('opaque', `"${r.opaque.verbatim}"${r.opaque.where ? ` at ${r.opaque.where}` : ''} — ${r.kind === 'assert' ? 'a check' : 'a step'} sungen cannot compare; QA reviews it`, i + 1));
43
- else if (r.low) out.push(finding('opaque', `${LOW_IDENTITY_FINDING} (${r.low}) — cannot prove "${r.sentence}" acts on the approved element`, i + 1));
73
+ if (r.opaque) {
74
+ const sha = opaqueStepSha(implementation, r, fp.steps[i]);
75
+ const isAccepted = accepted.some((a) => a.step === i + 1 && a.sha === sha);
76
+ const instead = replaces?.get(i);
77
+ opaque.push({ step: i + 1, sha, kind: r.kind, verbatim: r.opaque.verbatim, ...(r.opaque.where ? { where: r.opaque.where } : {}), ...(instead ? { replaces: instead } : {}), accepted: isAccepted });
78
+ if (!isAccepted) {
79
+ findings.push(finding('opaque', `"${r.opaque.verbatim}"${r.opaque.where ? ` at ${r.opaque.where}` : ''} — ${r.kind === 'assert' ? 'a check' : 'a step'} sungen cannot compare; QA reviews it${instead ? ` — replaces approved ${instead}` : ''}`, i + 1));
80
+ }
81
+ } else if (r.low) findings.push(finding('opaque', `${LOW_IDENTITY_FINDING} (${r.low}) — cannot prove "${r.sentence}" acts on the approved element`, i + 1));
44
82
  });
45
- return out;
83
+ return { findings, opaque };
46
84
  }
47
85
 
48
- function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: ContractFinding[]; used: Set<number> } {
86
+ function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: ContractFinding[]; used: Set<number>; replaces: Map<number, string> } {
49
87
  const findings: ContractFinding[] = [];
50
88
  const used = new Set<number>();
89
+ const replaces = new Map<number, string>();
51
90
  const taken = new Set<number>();
52
91
  entry.steps.forEach((locked, i) => {
53
92
  const g = fp.groups.findIndex((x, gi) => !taken.has(gi) && sameJson(x.titleStep, locked));
@@ -68,6 +107,13 @@ function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: C
68
107
  return;
69
108
  }
70
109
  candidates.forEach((m) => used.add(m));
110
+ // Only steps sungen cannot compare implement this approved step: the overlay sends them to QA's
111
+ // review (#923), naming the approved step. On the ladder they could only read "changed", which
112
+ // re-approving never clears.
113
+ if (candidates.every((m) => fp.rendered[m].opaque)) {
114
+ for (const m of candidates) replaces.set(m, `step ${i + 1} "${clip(approvedStepText(locked))}"`);
115
+ return;
116
+ }
71
117
  if (locked.kind === 'action') { findings.push(finding('hash-changed', `the code for "${locked.verb}" does something else: "${fp.rendered[candidates[0]].sentence}"`, i + 1)); return; }
72
118
  const judged = candidates.map((m) => compareAsserts(locked, fp.steps[m] as ContractStep));
73
119
  const rank = (k: string): number => (k === 'oracle-weakened' ? 0 : k === 'review' ? 1 : 2);
@@ -75,18 +121,21 @@ function implCodeFindings(entry: LockEntry, fp: TraceFingerprint): { findings: C
75
121
  findings.push(finding(best.class === 'review' ? 'hash-changed' : best.class, best.detail, i + 1));
76
122
  });
77
123
  fp.groups.forEach((g, gi) => { if (!taken.has(gi)) findings.push(finding('hash-changed', `test.step "${g.title}" implements no approved step`)); });
78
- return { findings, used };
124
+ return { findings, used, replaces };
79
125
  }
80
126
 
81
- function classifyOne(id: string, implementation: Implementation, entry: LockEntry, fp: TraceFingerprint): ContractFinding[] {
127
+ function classifyOne(id: string, implementation: Implementation, entry: LockEntry, fp: TraceFingerprint): Judged {
128
+ const accepted = entry.acceptedOpaque ?? [];
82
129
  if (implementation === 'impl:code') {
83
- const { findings, used } = implCodeFindings(entry, fp);
84
- return [...findings, ...overlays(fp, used)];
130
+ const { findings, used, replaces } = implCodeFindings(entry, fp);
131
+ const o = overlays(fp, implementation, accepted, used, replaces);
132
+ return { findings: [...findings, ...o.findings], opaque: o.opaque };
85
133
  }
86
134
  const ladder: ScenarioResult = classifyScenario(id, entry, {
87
135
  id, implementation, steps: fp.steps, hash: fp.hash, source: { file: '', name: id, hasId: true, lines: [], outline: false },
88
136
  });
89
- return [...ladder.findings, ...overlays(fp)];
137
+ const o = overlays(fp, implementation, accepted);
138
+ return { findings: [...ladder.findings, ...o.findings], opaque: o.opaque };
90
139
  }
91
140
 
92
141
  const runLabel = (r: TraceRun): string => `${r.record?.runId ?? '?'}#${r.result?.retry ?? '?'}`;
@@ -104,13 +153,18 @@ export function classifyTraceRuns(
104
153
  }
105
154
  // A trace that may not speak, beside one that may: the admissible one judges, the other is a warning.
106
155
  const warnings = others.map((r) => `${id}: another trace of this scenario was not used — ${r.reason ?? r.status}`);
107
- if (!entry) return { class: 'proposed', findings: [], hashes, runs: labels, warnings };
156
+ if (!entry) return { class: 'proposed', findings: [], hashes, runs: labels, warnings, opaque: [] };
108
157
  const perRun = admissible.map((r) => classifyOne(id, implementation, entry, fingerprints.get(r)!));
109
- const findings = perRun.flat();
110
- let cls = worstOf(perRun.flat());
158
+ const findings = perRun.flatMap((p) => p.findings);
159
+ const opaque = [...new Map(perRun.flatMap((p) => p.opaque).map((o) => [`${o.step}:${o.sha}`, o] as const)).values()]
160
+ .sort((a, b) => a.step - b.step || a.sha.localeCompare(b.sha));
161
+ for (const o of opaque.filter((x) => x.accepted)) {
162
+ warnings.push(`${id}: step ${o.step} — opaque ${o.kind === 'assert' ? 'check' : 'step'} "${clip(o.verbatim)}" accepted by QA (decision ${entry.decisionId}), not compared`);
163
+ }
164
+ let cls = worstOf(findings);
111
165
  if (new Set(hashes).size > 1) {
112
166
  findings.push(finding('nondeterministic', `${new Set(hashes).size} different fingerprints across ${admissible.length} runs — the runs took different branches`));
113
167
  if (severityOf(cls) <= severityOf('strengthened')) cls = 'nondeterministic';
114
168
  }
115
- return { class: cls, findings, hashes, runs: labels, warnings };
169
+ return { class: cls, findings, hashes, runs: labels, warnings, opaque };
116
170
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * step {kind: action|assert, verb, target?:{ref, also?, parent?, nth?}, value?, matcher?, expected?, negated?, table?}
5
5
  * lock qa/<kind>/<unit>/contracts.lock.yaml → {schema: 1, scenarios: {<scenario id>: LockEntry}}
6
- * entry {hash, steps, implementation, approvedAt, decisionId, humanApproved?}
6
+ * entry {hash, steps, implementation, approvedAt, decisionId, humanApproved?, acceptedOpaque?}
7
7
  *
8
8
  * A stored entry is checked the way the decision layer checks its records: a wrong type or an
9
9
  * UNKNOWN key is an error, because a field outside the hash could carry meaning the hash does not
@@ -49,6 +49,9 @@ export interface ContractScenario {
49
49
  source: { file: string; name: string; hasId: boolean; lines: number[]; outline: boolean };
50
50
  }
51
51
 
52
+ /** A reviewed opaque trace step QA accepted (#923): its 1-based position in the trace's steps and its sha (opaqueStepSha). */
53
+ export interface AcceptedOpaqueStep { step: number; sha: string }
54
+
52
55
  export interface LockEntry {
53
56
  hash: string;
54
57
  steps: ContractStep[];
@@ -56,8 +59,16 @@ export interface LockEntry {
56
59
  approvedAt: string;
57
60
  decisionId: string;
58
61
  humanApproved?: true;
62
+ /** Opaque trace steps a QA accepted on a terminal (`contract approve --accept-opaque`) — vouched for by `decisionId`. */
63
+ acceptedOpaque?: AcceptedOpaqueStep[];
59
64
  }
60
65
 
66
+ /**
67
+ * One opaque step of a scenario's trace(s), as `contract check` reports it and `--accept-opaque` shows it.
68
+ * `replaces` (impl:code): the approved step it stands in for — `step 5 "see [Title] heading"`.
69
+ */
70
+ export interface OpaqueStep { step: number; sha: string; kind: 'action' | 'assert'; verbatim: string; where?: string; replaces?: string; accepted: boolean }
71
+
61
72
  export const LOCK_SCHEMA = 1;
62
73
  export interface ContractLock { schema: typeof LOCK_SCHEMA; scenarios: Record<string, LockEntry> }
63
74
 
@@ -88,8 +99,10 @@ export const RULE_OF: Readonly<Partial<Record<FindingClass, string>>> = {
88
99
 
89
100
  const STEP_KEYS = new Set(['kind', 'verb', 'target', 'value', 'matcher', 'expected', 'negated', 'table']);
90
101
  const TARGET_KEYS = new Set(['ref', 'also', 'parent', 'nth']);
91
- const ENTRY_KEYS = new Set(['hash', 'steps', 'implementation', 'approvedAt', 'decisionId', 'humanApproved']);
102
+ const ENTRY_KEYS = new Set(['hash', 'steps', 'implementation', 'approvedAt', 'decisionId', 'humanApproved', 'acceptedOpaque']);
103
+ const ACCEPTED_KEYS = new Set(['step', 'sha']);
92
104
  const HASH_RE = /^[0-9a-f]{12}$/;
105
+ export const OPAQUE_SHA_RE = /^[0-9a-f]{16}$/;
93
106
 
94
107
  const isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
95
108
  const isStringList = (v: unknown): v is string[] => Array.isArray(v) && v.every((x) => typeof x === 'string');
@@ -118,6 +131,18 @@ export function stepErrors(s: unknown, at: string): string[] {
118
131
  return e;
119
132
  }
120
133
 
134
+ function acceptedOpaqueErrors(v: unknown, at: string): string[] {
135
+ if (!Array.isArray(v) || !v.length) return [`${at}: acceptedOpaque must be a non-empty list`];
136
+ return v.flatMap((a, i) => {
137
+ const where = `${at} acceptedOpaque ${i + 1}`;
138
+ if (!isObject(a)) return [`${where}: not a mapping`];
139
+ const e = unknownKeys(a, ACCEPTED_KEYS, where);
140
+ if (!(Number.isInteger(a.step) && (a.step as number) > 0)) e.push(`${where}: step must be a positive integer`);
141
+ if (typeof a.sha !== 'string' || !OPAQUE_SHA_RE.test(a.sha)) e.push(`${where}: sha must be 16 hex characters`);
142
+ return e;
143
+ });
144
+ }
145
+
121
146
  /** Errors in one stored lock entry (empty = well-formed). The hash itself is checked by the reader. */
122
147
  export function lockEntryErrors(raw: unknown, id: string): string[] {
123
148
  const at = `scenario ${id}`;
@@ -128,6 +153,10 @@ export function lockEntryErrors(raw: unknown, id: string): string[] {
128
153
  if (typeof raw.approvedAt !== 'string' || Number.isNaN(Date.parse(raw.approvedAt))) e.push(`${at}: approvedAt must be an ISO-8601 timestamp`);
129
154
  if (typeof raw.decisionId !== 'string' || !DECISION_ID_RE.test(raw.decisionId)) e.push(`${at}: decisionId must be D-<16 hex>`);
130
155
  if (raw.humanApproved !== undefined && raw.humanApproved !== true) e.push(`${at}: humanApproved is either true or absent`);
156
+ if (raw.acceptedOpaque !== undefined) {
157
+ e.push(...acceptedOpaqueErrors(raw.acceptedOpaque, at));
158
+ if (raw.humanApproved !== true) e.push(`${at}: acceptedOpaque needs humanApproved: true`);
159
+ }
131
160
  if (!Array.isArray(raw.steps)) e.push(`${at}: steps must be a list`);
132
161
  else raw.steps.forEach((s, i) => e.push(...stepErrors(s, `${at} step ${i + 1}`)));
133
162
  return e;
@@ -80,6 +80,7 @@ export function serializeLock(lock: ContractLock): string {
80
80
  scenarios[id] = {
81
81
  hash: e.hash, implementation: e.implementation, approvedAt: e.approvedAt, decisionId: e.decisionId,
82
82
  ...(e.humanApproved === true ? { humanApproved: true as const } : {}),
83
+ ...(e.acceptedOpaque?.length ? { acceptedOpaque: e.acceptedOpaque.map((a) => ({ step: a.step, sha: a.sha })) } : {}),
83
84
  steps: e.steps.map(orderedStep),
84
85
  };
85
86
  }
@@ -8,6 +8,8 @@
8
8
  * identities asked of the registered `traceIdentity` resolver (driver-ui) when one is given,
9
9
  * otherwise the cache, otherwise core's heuristic — `# identity low` marks those steps
10
10
  * approved a scenario QA already approved (no `@proposed`) is never rewritten
11
+ * one id, one two recorded tests claiming one id (#962) refuse the derive — both named with
12
+ * test file:line, NOTHING written for the unit: keeping one would drop the other's contract
11
13
  * safety redaction by field meaning, machine paths scrubbed, then the secret scanner and the
12
14
  * semantic credential check over the exact text: any hit → NOTHING is written
13
15
  */
@@ -22,8 +24,10 @@ import { scrubKnownSecrets, scrubMachinePaths, secretScanHits, SecretHit } from
22
24
  import { idOfTitle, splitFeatureBlocks } from './free-feature-blocks';
23
25
  import { sensitiveLiteralHits } from './lock-secret-guard';
24
26
  import { scenariosOfFeatureFile } from './normalize-feature-steps';
27
+ import { featureFilesFor } from '../harness/unit-paths';
25
28
  import { CONTROL_CHARS, freeFeatureText, renderTraceSteps, scenarioBlockText } from './render-free-gherkin';
26
29
  import { collectRuns, TraceRun } from './trace/collect-trace-evidence';
30
+ import { idsClaimedByTwoTests } from './trace/scenario-id-claims';
27
31
  import { readIdentityCache } from './trace/identity-cache';
28
32
  import type { TraceIdentityResolver } from './trace/target-identity';
29
33
  import { identitiesFor, resolveWithDriver } from './trace/trace-identity-resolution';
@@ -35,9 +39,17 @@ export interface DeriveResult {
35
39
  file: string;
36
40
  written: boolean;
37
41
  scenarios: Array<{ id: string; title: string; steps: number; low: number; opaque: number }>;
38
- skipped: Array<{ id: string; reason: string }>;
42
+ /** `approved: true` — skipped because QA already approved it (derive never rewrites one), not for want of a trace. */
43
+ skipped: Array<{ id: string; reason: string; approved?: true }>;
39
44
  refused?: SecretHit[];
45
+ /**
46
+ * Ids two or more recorded tests claim (#962), each test as `file:line "title" (run record <path>)` —
47
+ * nothing was written. `staleRecords`: older test-results/ records that are a claim's only source.
48
+ */
49
+ duplicateIds?: Array<{ id: string; tests: string[]; staleRecords?: string[] }>;
40
50
  note?: string;
51
+ /** A derived id another `.feature` of the unit already uses (#939) — `contract check` and approve would trip on it later. */
52
+ warnings?: string[];
41
53
  }
42
54
 
43
55
  const MACHINE_PATH = /(?:\/Users|\/home)\/[^/\s"'`]+\/|[A-Za-z]:\\Users\\/;
@@ -58,13 +70,42 @@ function semanticHits(text: string, label: string): SecretHit[] {
58
70
  } finally { fs.rmSync(dir, { recursive: true, force: true }); }
59
71
  }
60
72
 
73
+ /**
74
+ * The derived ids another `.feature` of the unit already carries (#939): `contract check` reports an id
75
+ * used twice and approve refuses it, so say it now — when it is still one rename away — not at check.
76
+ */
77
+ function reusedIdWarnings(unit: { name: string; dir: string }, freeFile: string, ids: string[]): string[] {
78
+ const owners = new Map<string, string>();
79
+ for (const f of featureFilesFor(unit.dir, unit.name)) {
80
+ if (path.resolve(f) === path.resolve(freeFile) || !fs.existsSync(f)) continue;
81
+ try {
82
+ for (const sc of scenariosOfFeatureFile(f)) if (!owners.has(sc.id)) owners.set(sc.id, path.basename(f));
83
+ } catch { /* an unparseable sibling is `contract check`'s to report */ }
84
+ }
85
+ return ids.filter((id) => owners.has(id)).map((id) =>
86
+ `${id} is already the ID of a scenario in ${owners.get(id)} — the derived scenario reuses it; give the free test (its title's leading ID, or its @tag) or that scenario its own ID, or \`sungen contract check\` reports it twice and approval refuses it`);
87
+ }
88
+
89
+ /**
90
+ * Nothing was derived and nothing explains it away: no test of the unit left an admissible trace
91
+ * (#932). Not when the scenarios are approved (derive leaves those alone by design), up to date,
92
+ * refused, or the project is mobile (`note`).
93
+ */
94
+ export function nothingDerived(r: DeriveResult): boolean {
95
+ return !r.note && !r.refused?.length && !r.duplicateIds?.length && r.scenarios.length === 0 && !r.skipped.some((s) => s.approved);
96
+ }
97
+
61
98
  export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
62
99
  const unitBase = path.basename(i.unit.name);
63
100
  const file = path.join(i.unit.dir, 'features', `${unitBase}.free.feature`);
64
101
  const result: DeriveResult = { file, written: false, scenarios: [], skipped: [] };
65
102
  if ((readCapabilities(i.projectRoot).platform ?? '').toLowerCase() === 'mobile') return { ...result, note: MOBILE_NOT_SUPPORTED };
66
103
  const grep = i.grep ? new RegExp(i.grep, 'i') : null;
67
- const found = collectRuns(i.projectRoot, (id, t) => (grep ? grep.test(`${id} ${t.title} ${t.file}`) : belongsToUnit(t.file, unitBase.toLowerCase())), 0);
104
+ const found = collectRuns(i.projectRoot, (id, t) => (grep ? grep.test(`${id} ${t.title} ${t.file}`) : belongsToUnit(t.file, unitBase.toLowerCase())), null);
105
+ const claimed = idsClaimedByTwoTests(i.projectRoot, found.runs);
106
+ if (claimed.size) {
107
+ return { ...result, duplicateIds: [...claimed].sort(([a], [b]) => a.localeCompare(b, 'en')).map(([id, c]) => ({ id, tests: c.tests, ...(c.staleRecords.length ? { staleRecords: c.staleRecords } : {}) })) };
108
+ }
68
109
  const byId = new Map<string, TraceRun[]>();
69
110
  for (const r of found.runs) byId.set(r.scenarioId, [...(byId.get(r.scenarioId) ?? []), r]);
70
111
  const admissible = (runs: TraceRun[]): TraceRun[] => runs.filter((r) => r.status === 'admissible' && r.trace)
@@ -79,7 +120,7 @@ export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
79
120
  for (const id of [...byId.keys()].sort((a, b) => a.localeCompare(b, 'en'))) {
80
121
  const runs = admissible(byId.get(id)!);
81
122
  if (!runs.length) { result.skipped.push({ id, reason: byId.get(id)![0].reason ?? 'no admissible trace' }); continue; }
82
- if (kept.some((b) => b.id === id && !b.proposed)) { result.skipped.push({ id, reason: 'approved — derive never rewrites an approved scenario; use `sungen contract check --trace`' }); continue; }
123
+ if (kept.some((b) => b.id === id && !b.proposed)) { result.skipped.push({ id, reason: 'approved — derive never rewrites an approved scenario; use `sungen contract check --trace`', approved: true }); continue; }
83
124
  const secrets = new Set<string>();
84
125
  const rendered = runs.map((r) => {
85
126
  const steps = traceIntentSteps(r.trace!);
@@ -98,6 +139,8 @@ export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
98
139
  result.scenarios.push({ id, title, steps: rendered[0].length, low: rendered[0].filter((s) => s.low).length, opaque: rendered[0].filter((s) => s.opaque).length });
99
140
  }
100
141
  if (!result.scenarios.length) return result;
142
+ const reused = reusedIdWarnings(i.unit, file, result.scenarios.map((x) => x.id));
143
+ if (reused.length) result.warnings = reused;
101
144
 
102
145
  const text = freeFeatureText(unitBase, [...blocks.values()]);
103
146
  const label = path.basename(file);
@@ -4,12 +4,14 @@
4
4
  * HEAD is read once (one `git ls-tree`, bounded by the caller's timeout). A unit with no lock in the
5
5
  * working tree AND none in HEAD has no contract to break and is skipped without parsing anything.
6
6
  * Every other unit gets its verdict rebuilt by buildContractVerdict — the same function `contract
7
- * check` runs — and anything but `pass` blocks. A lock deleted from the working tree still counts
7
+ * check` runs — and anything but `pass` blocks. Its trace part gets the guard's ~3 s at most:
8
+ * SUNGEN_TRACE_VERDICT_BUDGET_MS can shorten that here, never lengthen it (#926). A lock deleted from the working tree still counts
8
9
  * while HEAD holds it. If git FAILS in a checkout that has .git while a delivered unit has a lock,
9
10
  * this throws (GitUnavailableError): the guard asks rather than calling an unverifiable lock clean.
10
11
  */
11
12
  import * as fs from 'fs';
12
13
  import { buildContractVerdict, ContractUnit, VerdictValue } from './build-contract-verdict';
14
+ import { guardTraceBudgetMs } from './build-trace-conformance';
13
15
  import { CLI_GIT_TIMEOUT_MS, GitUnavailableError, headSnapshot, posixRel } from './contract-git';
14
16
  import { HARD_CLASSES } from './contract-schema';
15
17
  import { lockPathFor } from './contracts-lock-file';
@@ -24,7 +26,7 @@ export function contractBlockers(projectRoot: string, units: ContractUnit[], opt
24
26
  const out: ContractBlocker[] = [];
25
27
  for (const unit of unique) {
26
28
  if (!hasWorkingLock(unit) && !snap.blobs?.has(posixRel(projectRoot, lockPathFor(unit.dir)))) continue;
27
- const v = buildContractVerdict(projectRoot, unit, { snapshot: snap });
29
+ const v = buildContractVerdict(projectRoot, unit, { snapshot: snap, traceBudgetMs: guardTraceBudgetMs() });
28
30
  if (v.verdict === 'pass') continue;
29
31
  const worst = v.scenarios.find((s) => HARD_CLASSES.has(s.class)) ?? v.scenarios.find((s) => s.class === 'hash-changed');
30
32
  const headline = worst
@@ -9,13 +9,17 @@
9
9
  * The decision goes first: a crash between the two leaves a record without a lock entry, never an
10
10
  * entry pointing at nothing. Only `sungen contract approve` calls this after its TTY confirmation;
11
11
  * the confirmation itself lives there (approve-contract.ts).
12
+ *
13
+ * `--accept-opaque` (#923, accept-opaque-steps.ts) writes the same kind of decision for ONE scenario,
14
+ * listing it at its hash WITH the opaque steps QA accepted (step + sha); its lock entry is rewritten to
15
+ * point at that decision and carries the same list, which verify-approval-decisions.ts cross-checks.
12
16
  */
13
17
  import * as path from 'path';
14
18
  import { appendDecision } from '../decisions/append-decision';
15
19
  import { isValidScope } from '../decisions/decision-file';
16
20
  import type { JsonValue } from '../decisions/decision-schema';
17
21
  import { gitUserName } from '../decisions/decision-source';
18
- import { ContractLock, ContractScenario, LOCK_SCHEMA } from './contract-schema';
22
+ import { AcceptedOpaqueStep, ContractLock, ContractScenario, LOCK_SCHEMA } from './contract-schema';
19
23
  import { lockEntryFor, lockPathFor, serializeLock, writeLockFile } from './contracts-lock-file';
20
24
  import { assertNoLockSecrets } from './lock-secret-guard';
21
25
 
@@ -30,6 +34,18 @@ export interface ApprovalWrite {
30
34
  }
31
35
 
32
36
  export const DEFAULT_APPROVAL_REASON = 'QA reviewed the normalized Gherkin contract';
37
+ export const DEFAULT_OPAQUE_REASON = 'QA reviewed the opaque step(s) sungen cannot compare';
38
+
39
+ export interface OpaqueAcceptanceWrite {
40
+ projectRoot: string;
41
+ unitDir: string;
42
+ unit: string;
43
+ /** The scenario as it reads now — its hash equals its approved lock entry's. */
44
+ scenario: ContractScenario;
45
+ accepted: AcceptedOpaqueStep[];
46
+ reason?: string;
47
+ lock: ContractLock;
48
+ }
33
49
 
34
50
  /** The decision scope of a unit directory (`screens/login`, `api/flows/pay`…). */
35
51
  export function scopeOfUnitDir(projectRoot: string, unitDir: string): string {
@@ -68,3 +84,27 @@ export function recordContractApproval(w: ApprovalWrite): { decisionId: string;
68
84
  writeLockFile(lockPathFor(w.unitDir), lock);
69
85
  return { decisionId: decision.entry.id, lock };
70
86
  }
87
+
88
+ function lockWithAcceptance(w: OpaqueAcceptanceWrite, decisionId: string, approvedAt: string): ContractLock {
89
+ const entry = { ...lockEntryFor(w.scenario, decisionId, approvedAt), acceptedOpaque: w.accepted.map((a) => ({ step: a.step, sha: a.sha })) };
90
+ return { schema: LOCK_SCHEMA, scenarios: { ...w.lock.scenarios, [w.scenario.id]: entry } };
91
+ }
92
+
93
+ export function recordOpaqueAcceptance(w: OpaqueAcceptanceWrite): { decisionId: string; lock: ContractLock } {
94
+ const scope = scopeOfUnitDir(w.projectRoot, w.unitDir);
95
+ assertNoLockSecrets([w.scenario], serializeLock(lockWithAcceptance(w, 'D-0000000000000000', new Date().toISOString())));
96
+ const sc = w.scenario;
97
+ const accepted = w.accepted.map((a) => ({ step: a.step, sha: a.sha }));
98
+ const payload: Record<string, JsonValue> = {
99
+ unit: w.unit, scenarios: [{ id: sc.id, hash: sc.hash, implementation: sc.implementation, acceptedOpaque: accepted }], removed: [],
100
+ };
101
+ const decision = appendDecision(w.projectRoot, scope, {
102
+ kind: 'contract-approval', anchors: [], visibility: 'internal', valid_from: new Date().toISOString(), payload,
103
+ text: `Accepted the reviewed opaque step(s) ${[...new Set(accepted.map((a) => a.step))].join(', ')} of ${sc.id} (${w.unit})`,
104
+ reason: w.reason?.trim() || DEFAULT_OPAQUE_REASON,
105
+ source: { by: gitUserName(w.projectRoot), where: 'cli' },
106
+ });
107
+ const lock = lockWithAcceptance(w, decision.entry.id, decision.entry.valid_from);
108
+ writeLockFile(lockPathFor(w.unitDir), lock);
109
+ return { decisionId: decision.entry.id, lock };
110
+ }
@@ -22,6 +22,11 @@ import { isPageLevel } from './trace/trace-identity-resolution';
22
22
  import type { IntentStep } from './trace/trace-steps';
23
23
 
24
24
  export const CONTROL_CHARS = /[\u0000-\u001f\u007f\u2028\u2029]/g;
25
+ /** Text from a trace or report shown in one line of a finding: control characters dropped, at most `max` characters. */
26
+ export function clip(s: string, max = 120): string {
27
+ const t = s.replace(CONTROL_CHARS, '');
28
+ return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
29
+ }
25
30
 
26
31
  export interface ResolvedTarget { identity: TargetIdentity; facts?: TargetFacts }
27
32
  export interface RenderedStep {
@@ -3,33 +3,56 @@
3
3
  * resolver (driver-ui — a real browser) about every target of the unit's admissible traces and
4
4
  * cache the answers (DOS-14.5). The verdict itself never calls a browser; it reads the cache, so
5
5
  * the CLI and the Claude Code guard keep building the same verdict from the same files.
6
+ *
7
+ * What it reports is split for QA (#966): a WARNING only when something needs attention — no
8
+ * resolver, a mobile project, a target the browser could not name (its steps stay opaque). "Every
9
+ * target was already known" and "N new targets identified" are information, in plain words.
6
10
  */
7
- import * as fs from 'fs';
8
11
  import { capabilityRegistry } from '../capabilities/registry';
9
12
  import { readCapabilities } from '../harness/capability';
10
13
  import { MOBILE_NOT_SUPPORTED } from './build-trace-conformance';
11
- import { lockPathFor } from './contracts-lock-file';
12
- import { contractFeatureFiles, loadContractScenarios } from './normalize-feature-steps';
14
+ import { loadContractScenarios } from './normalize-feature-steps';
13
15
  import { collectScenarioEvidence } from './trace/collect-trace-evidence';
16
+ import { readIdentityCache } from './trace/identity-cache';
17
+ import { unitContract } from './trace/trace-freshness';
14
18
  import type { TraceIdentityResolver } from './trace/target-identity';
15
19
  import { resolveWithDriver } from './trace/trace-identity-resolution';
16
20
 
17
- const mtimeMs = (f: string): number => { try { return fs.statSync(f).mtimeMs; } catch { return 0; } };
21
+ export interface TraceIdentityOutcome { warnings: string[]; info: string[] }
18
22
 
19
23
  /** The first registered capability that can resolve trace identities (UI: driver-ui), if any. */
20
24
  export function registeredTraceResolver(): TraceIdentityResolver | undefined {
21
25
  return capabilityRegistry.all().find((c) => typeof c.traceIdentity === 'function')?.traceIdentity;
22
26
  }
23
27
 
24
- export async function resolveUnitTraceIdentities(projectRoot: string, unit: { name: string; dir: string }, resolver = registeredTraceResolver()): Promise<string[]> {
28
+ export async function traceIdentityOutcome(projectRoot: string, unit: { name: string; dir: string }, resolver = registeredTraceResolver()): Promise<TraceIdentityOutcome> {
29
+ const none: TraceIdentityOutcome = { warnings: [], info: [] };
25
30
  const traced = loadContractScenarios(unit.dir, unit.name).filter((s) => s.implementation !== 'compiled');
26
- if (!traced.length) return [];
27
- if ((readCapabilities(projectRoot).platform ?? '').toLowerCase() === 'mobile') return [MOBILE_NOT_SUPPORTED];
28
- if (!resolver) return ['no trace identity resolver is registered (install @sungen/driver-ui) — every target identity stays low, so nothing can match'];
29
- const notBefore = Math.max(mtimeMs(lockPathFor(unit.dir)), ...contractFeatureFiles(unit.dir, unit.name).map(mtimeMs));
30
- const evidence = collectScenarioEvidence(projectRoot, new Set(traced.map((s) => s.id)), notBefore);
31
+ if (!traced.length) return none;
32
+ if ((readCapabilities(projectRoot).platform ?? '').toLowerCase() === 'mobile') return { warnings: [MOBILE_NOT_SUPPORTED], info: [] };
33
+ if (!resolver) return { warnings: ['no trace identity resolver is registered (install @sungen/driver-ui) — every target identity stays low, so nothing can match'], info: [] };
34
+ const evidence = collectScenarioEvidence(projectRoot, new Set(traced.map((s) => s.id)), unitContract(projectRoot, unit));
31
35
  const traces = [...evidence.runs.values()].flat().filter((r) => r.status === 'admissible' && r.trace).map((r) => r.trace!);
32
- if (!traces.length) return [];
36
+ if (!traces.length) return none;
33
37
  const r = await resolveWithDriver(projectRoot, traces, resolver);
34
- return [`trace identity: ${r.asked} target(s) resolved by ${r.resolver || 'the cache'}${r.blockedRequests ? `, ${r.blockedRequests} replay request(s) blocked` : ''}`];
38
+ const { cache } = readIdentityCache(projectRoot);
39
+ const total = r.keys.length;
40
+ const unnamed = r.keys.filter((k) => cache.entries.get(k)?.identityConfidence !== 'high').length;
41
+ const blocked = r.blockedRequests ? `; the replayed pages tried ${r.blockedRequests} request(s), all blocked` : '';
42
+ const known = total - r.asked;
43
+ const info = r.asked
44
+ ? `trace identity: ${r.asked} new target(s) identified in a real browser (${r.resolver})${known ? `, ${known} already known` : ''}${blocked}`
45
+ : `trace identity: all ${total} target(s) were already identified (identity cache) — nothing new to resolve`;
46
+ const warnings = unnamed
47
+ ? [`trace identity: ${unnamed} of ${total} target(s) could not be identified (no unique accessible name) — the steps on them stay opaque, for QA to review, until the test targets a unique, named element`]
48
+ : [];
49
+ return { warnings, info: [info] };
50
+ }
51
+
52
+ /**
53
+ * What `contract check --trace` adds to the verdict's warnings: only what needs attention (#966).
54
+ * The plain-words information is `traceIdentityOutcome(...).info`.
55
+ */
56
+ export async function resolveUnitTraceIdentities(projectRoot: string, unit: { name: string; dir: string }, resolver = registeredTraceResolver()): Promise<string[]> {
57
+ return (await traceIdentityOutcome(projectRoot, unit, resolver)).warnings;
35
58
  }