@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,15 +7,21 @@
7
7
  * stale the run manifest (`sungen changed`) selects the scenario: its steps/tags, a selector or a
8
8
  * test-data key it uses, or a project input changed since the recorded run
9
9
  * triage the rows of `.sungen/triage/<unit>.json` for the case, only when that triage is not
10
- * older than the case's result files (a re-run voids it)
10
+ * older than the case's result files (a re-run voids it) — unless a manual classification
11
+ * in force judged this very red (manual-triage-decisions.ts, #928): that one wins
12
+ * evidence a red case's hash of its red final attempts (file, title, status, start, duration,
13
+ * messages): what a manual classification is bound to
11
14
  */
15
+ import { createHash } from 'crypto';
12
16
  import * as fs from 'fs';
13
17
  import * as path from 'path';
18
+ import { canonicalJson } from '../decisions/decision-identity';
14
19
  import { loadPlaywrightReport } from '../exporters/playwright-report-parser';
15
20
  import type { PlaywrightResult } from '../exporters/types';
16
21
  import { buildRunManifest, readRunManifest, selectChanged } from '../harness/run-manifest';
17
22
  import { readTriageReport } from '../harness/triage-unit';
18
23
  import type { TriageClass } from '../harness/triage';
24
+ import { manualClassificationFor, manualClassifications } from './manual-triage-decisions';
19
25
  import type { FeatureCase } from './test-case-identity';
20
26
  import type { TestCaseResult, TestCaseState } from './test-case-state-shape';
21
27
  import { generatedDirs, RESULT_FILE } from './unit-artifact-probes';
@@ -24,7 +30,7 @@ const RED = new Set<PlaywrightResult['status']>(['failed', 'timedOut', 'interrup
24
30
  /** The class a case shows when its rows disagree: what needs a human first. */
25
31
  const TRIAGE_PRECEDENCE: TriageClass[] = ['product-candidate', 'unclassified', 'test-defect', 'environment', 'known-defect', 'blocked-capability', 'flaky'];
26
32
 
27
- export interface CaseOutcome { result: TestCaseResult; failedResults: number; resultFiles: string[]; stale?: string; triage?: TestCaseState['triage'] }
33
+ export interface CaseOutcome { result: TestCaseResult; failedResults: number; resultFiles: string[]; stale?: string; triage?: TestCaseState['triage']; evidence?: string }
28
34
 
29
35
  const ownsTitle = (key: string, title: string): boolean => key === title || key.endsWith(` > ${title}`)
30
36
  || key.startsWith(`${title} — `) || key.includes(` > ${title} — `);
@@ -63,16 +69,28 @@ function triageRows(projectRoot: string, unitId: string): { at: number; rows: Ar
63
69
  return { at: report && typeof report.generatedAt === 'string' ? Date.parse(report.generatedAt) : NaN, rows };
64
70
  }
65
71
 
72
+ /** The red final attempts of one case, hashed: the same red reads the same; a re-run (new start, duration) does not. */
73
+ function redEvidence(projectRoot: string, reds: Array<{ file: string; key: string; r: PlaywrightResult }>): string {
74
+ const rows = reds.map(({ file, key, r }) => ({
75
+ file: path.relative(projectRoot, file).split(path.sep).join('/'), key, status: r.status,
76
+ startTime: r.startTime ?? null, duration: r.duration ?? null, messages: r.errorMessages ?? (r.error ? [r.error] : []),
77
+ })).sort((a, b) => (`${a.file}\n${a.key}` < `${b.file}\n${b.key}` ? -1 : 1));
78
+ return createHash('sha256').update(canonicalJson(rows)).digest('hex');
79
+ }
80
+
66
81
  export function caseOutcomes(projectRoot: string, unitDir: string, unit: string, unitId: string, cases: FeatureCase[]): CaseOutcome[] {
67
82
  const byFeature = resultsByFeature(projectRoot, unit, unitId);
68
83
  const stale = staleReasons(projectRoot, unitDir, unit);
69
84
  const triage = triageRows(projectRoot, unitId);
85
+ const manual = manualClassifications(projectRoot, unitDir);
70
86
  return cases.map((c) => {
71
87
  const statuses: PlaywrightResult['status'][] = [];
72
88
  const files: string[] = [];
89
+ const reds: Array<{ file: string; key: string; r: PlaywrightResult }> = [];
73
90
  for (const { file, report } of byFeature.get(c.featureBase) ?? []) {
74
- const mine = [...report.entries()].filter(([key]) => ownsTitle(key, c.title)).map(([, r]) => r.status);
75
- if (mine.length) { statuses.push(...mine); files.push(file); }
91
+ const mine = [...report.entries()].filter(([key]) => ownsTitle(key, c.title));
92
+ if (mine.length) { statuses.push(...mine.map(([, r]) => r.status)); files.push(file); }
93
+ for (const [key, r] of mine) if (RED.has(r.status)) reds.push({ file, key, r });
76
94
  }
77
95
  const result: TestCaseResult = statuses.some((s) => RED.has(s)) ? 'failed'
78
96
  : statuses.includes('passed') ? 'passed' : statuses.includes('skipped') ? 'skipped' : 'never-run';
@@ -81,10 +99,13 @@ export function caseOutcomes(projectRoot: string, unitDir: string, unit: string,
81
99
  resultFiles: files.map((f) => path.relative(projectRoot, f).split(path.sep).join('/')),
82
100
  };
83
101
  if (result !== 'never-run' && stale[c.title]) out.stale = stale[c.title];
102
+ if (result === 'failed') out.evidence = redEvidence(projectRoot, reds);
84
103
  const current = !Number.isNaN(triage.at) && files.every((f) => mtime(f) <= triage.at);
85
104
  const rows = current ? triage.rows.filter((r) => rowOwns(r.scenario, c.title)) : [];
86
105
  const worst = TRIAGE_PRECEDENCE.find((cls) => rows.some((r) => r.class === cls));
87
106
  if (worst) out.triage = { class: worst, ruleId: rows.find((r) => r.class === worst)!.ruleId };
107
+ const byHand = manualClassificationFor(manual, c.id, out.evidence);
108
+ if (byHand) out.triage = { class: byHand.class, ruleId: 'manual', decision: byHand.decision };
88
109
  return out;
89
110
  });
90
111
  }
@@ -8,14 +8,15 @@
8
8
  * trigger — what the case needs otherwise, first match wins:
9
9
  * never run → run · result older than the scenario (run manifest) → rerun-stale ·
10
10
  * red with no current triage, or left `unclassified` by it → triage (a classification
11
- * is still owed) · triaged product-candidate → a QA decision
12
- * A pass — Pass (Mocked) included — needs nothing. A red triaged test-defect / environment has been
13
- * judged; the triage report and the deliverable carry it (no P0 node — a P1 follow-up).
11
+ * is still owed) · triaged product-candidate → a QA decision · triaged environment →
12
+ * rerun-environment (once it is up) · triaged test-defect → fix-test (#928)
13
+ * A pass — Pass (Mocked) included — needs nothing. The class may be `sungen triage`'s or a manual
14
+ * classification in force (manual-triage-decisions.ts); test-case-results.ts decides which.
14
15
  */
15
16
  import type { TestCaseState } from './test-case-state-shape';
16
17
 
17
18
  export type TestCaseExemption = 'manual' | 'requires' | 'known-defect' | 'deferred' | 'blocked-capability';
18
- export type TestCaseNeed = 'run' | 'rerun-stale' | 'triage' | 'product-candidate';
19
+ export type TestCaseNeed = 'run' | 'rerun-stale' | 'triage' | 'product-candidate' | 'rerun-environment' | 'fix-test';
19
20
 
20
21
  const tagged = (tc: TestCaseState, tag: string): boolean => tc.tags.some((t) => t === tag || t.startsWith(`${tag}:`));
21
22
 
@@ -36,6 +37,8 @@ export function triggerOf(tc: TestCaseState): TestCaseNeed | null {
36
37
  if (tc.result === 'failed') {
37
38
  if (!tc.triage || tc.triage.class === 'unclassified') return 'triage';
38
39
  if (tc.triage.class === 'product-candidate') return 'product-candidate';
40
+ if (tc.triage.class === 'environment') return 'rerun-environment';
41
+ if (tc.triage.class === 'test-defect') return 'fix-test';
39
42
  }
40
43
  return null;
41
44
  }
@@ -52,8 +52,13 @@ export interface TestCaseState {
52
52
  resultFiles: string[];
53
53
  /** Set when the scenario changed after the run that produced its result (run manifest). */
54
54
  stale?: string;
55
- /** The current `sungen triage` row for a red case — absent when triage has not judged this result. */
56
- triage?: { class: TriageClass; ruleId: string };
55
+ /**
56
+ * The class of a red case: a manual classification in force (`ruleId: 'manual'` and its decision id,
57
+ * #928), else the current `sungen triage` row — absent when neither has judged this result.
58
+ */
59
+ triage?: { class: TriageClass; ruleId: string; decision?: string };
60
+ /** Hash of the red final attempts the case accounts for (failed only) — a manual classification is bound to it; a re-run changes it. */
61
+ evidence?: string;
57
62
  /** Spec anchors the case was generated from (manifest), and whether they moved since. */
58
63
  anchors: Array<{ anchor: string; change: 'keep' | 'regenerate' | 'retire' | 'unknown' }>;
59
64
  findings: LocatedFinding[];
@@ -9,6 +9,7 @@ import { parse as parseYaml } from 'yaml';
9
9
  import { featureFilesFor } from '../harness/unit-paths';
10
10
  import { readTextFile } from '../harness/read-text';
11
11
  import { loadPlaywrightReport } from '../exporters/playwright-report-parser';
12
+ import { isDraftDeliverable } from '../exporters/csv-exporter';
12
13
 
13
14
  /** `<f>-test-result[.<os>][.<env>].json` — a mobile run writes the OS into the name, an env run the env (#868). */
14
15
  export const RESULT_FILE = /-test-result(?:\.[^.]+)*\.json$/;
@@ -72,6 +73,7 @@ export function countFailedResults(resultFiles: string[]): number {
72
73
 
73
74
  export function hasDeliverableFor(projectRoot: string, slug: string): boolean {
74
75
  try {
75
- return fs.readdirSync(path.join(projectRoot, 'qa', 'deliverables')).some((f) => f.startsWith(`${slug}-`));
76
+ // A `--preview` DRAFT is not a deliverable (#965).
77
+ return fs.readdirSync(path.join(projectRoot, 'qa', 'deliverables')).some((f) => f.startsWith(`${slug}-`) && !isDraftDeliverable(f));
76
78
  } catch { return false; } // nothing exported yet
77
79
  }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Which run a unit's results come from, and whether they still describe the suite (#942) — so `next`
3
+ * and the delivery checklist never read a months-old results JSON in silence. Read-only, like the rest
4
+ * of the graph's facts.
5
+ *
6
+ * run each result file's run start, as Playwright records it (`stats.startTime`); a file
7
+ * without one is dated by its own change time, and says so
8
+ * older an input changed after the results it feeds were recorded:
9
+ * per feature its `.feature` and compiled `<base>.spec.ts` ↔ its `<base>-test-result*.json`
10
+ * unit-wide requirements/spec.md ↔ the oldest result file
11
+ * Not contracts.lock.yaml: approving the contract after a run is the normal order, and it
12
+ * changes no test — the contract check judges the lock, not the results.
13
+ * A file's "change time" is its last commit when git tracks it unchanged, else its file
14
+ * time. Results are "recorded" at the later of their run start and their own change time.
15
+ * So a fresh checkout (every file the checkout's time) and results committed in the same
16
+ * commit as the edit they ran against read fresh; an edit, or a commit, after them does not.
17
+ * env SUNGEN_ENV names an env no result file is for: readers fall back to another env's results
18
+ * Advisory: it states what it sees and warns; it decides no step and changes no exit code.
19
+ */
20
+ import { execFileSync } from 'child_process';
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+ import { catalogIdFromScreenDir, featureFilesFor } from '../harness/unit-paths';
24
+ import { generatedDirs, RESULT_FILE } from './unit-artifact-probes';
25
+
26
+ export interface ResultsFreshness {
27
+ files: Array<{ file: string; runAt: string; source: 'run' | 'file' }>;
28
+ /** The oldest result file's run — what the counts are at least as old as. */
29
+ oldestRunAt: string;
30
+ newestRunAt: string;
31
+ /** How the oldest run is dated: Playwright's run start, or (none recorded) the file's change time. */
32
+ runAtSource: 'run' | 'file';
33
+ /** Inputs changed after the results they feed were recorded. */
34
+ newerInputs: Array<{ file: string; changedAt: string; source: 'commit' | 'file' }>;
35
+ /** SUNGEN_ENV, when no result file is for it. */
36
+ missingEnv?: string;
37
+ /** One line: what makes these results untrustworthy — absent when nothing does. */
38
+ warning?: string;
39
+ }
40
+
41
+ type Change = { ms: number; source: 'commit' | 'file' };
42
+ /** `next` also runs inside the Claude Code hooks (10 s budget, the contract's git calls included): a slow git dates by file time instead. */
43
+ const GIT_TIMEOUT_MS = 2_000;
44
+ /** Commit times have one-second resolution: an edit and its results in one commit tie. */
45
+ const TIE_MS = 1_000;
46
+ const posix = (p: string): string => p.split(path.sep).join('/');
47
+ const real = (p: string): string => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
48
+ const iso = (ms: number): string => new Date(ms).toISOString();
49
+
50
+ /** project root → its git work tree (null outside one), asked once per process: `next` reads every unit. */
51
+ const workTrees = new Map<string, string | null>();
52
+
53
+ function git(cwd: string, args: string[]): string | null {
54
+ try { return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: GIT_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }).toString('utf8'); } catch { return null; }
55
+ }
56
+
57
+ /**
58
+ * When each file last changed: its last commit when git tracks it and it is unchanged, else its file
59
+ * time (no git, uncommitted, untracked, ignored, or git failed). Two git calls for the whole set.
60
+ */
61
+ function changeTimes(projectRoot: string, files: string[]): (abs: string) => Change | null {
62
+ const mtime = (abs: string): Change | null => { try { return { ms: fs.statSync(abs).mtimeMs, source: 'file' }; } catch { return null; } };
63
+ if (!workTrees.has(projectRoot)) workTrees.set(projectRoot, git(projectRoot, ['rev-parse', '--show-toplevel'])?.trim() || null);
64
+ const top = workTrees.get(projectRoot);
65
+ if (!top) return mtime;
66
+ const topReal = real(top);
67
+ const rels = files.map((f) => posix(path.relative(topReal, real(f)))).filter((r) => r && !r.startsWith('..'));
68
+ if (!rels.length) return mtime;
69
+ const status = git(topReal, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--', ...rels]);
70
+ const log = git(topReal, ['log', '-n', '500', '--format=%x01%ct', '--name-only', '--', ...rels]);
71
+ if (status === null || log === null) return mtime;
72
+ const dirty = new Set<string>();
73
+ const entries = status.split('\0');
74
+ for (let i = 0; i < entries.length; i++) {
75
+ const e = entries[i];
76
+ if (e.length < 4) continue;
77
+ dirty.add(e.slice(3));
78
+ if (e[0] === 'R' || e[0] === 'C') i++; // a rename/copy carries its source path next
79
+ }
80
+ const committed = new Map<string, number>();
81
+ let at = 0;
82
+ for (const line of log.split('\n')) {
83
+ if (line.startsWith('\x01')) at = Number(line.slice(1)) * 1000;
84
+ else if (line.trim() && at && !committed.has(line.trim())) committed.set(line.trim(), at);
85
+ }
86
+ return (abs) => {
87
+ const rel = posix(path.relative(topReal, real(abs)));
88
+ const t = committed.get(rel);
89
+ return t !== undefined && !dirty.has(rel) && fs.existsSync(abs) ? { ms: t, source: 'commit' } : mtime(abs);
90
+ };
91
+ }
92
+
93
+ function runStartOf(file: string): number | null {
94
+ try {
95
+ const t = (JSON.parse(fs.readFileSync(file, 'utf8')) as { stats?: { startTime?: unknown } })?.stats?.startTime;
96
+ const ms = typeof t === 'string' ? Date.parse(t) : NaN;
97
+ return Number.isNaN(ms) ? null : ms;
98
+ } catch { return null; }
99
+ }
100
+
101
+ /** Every result file of the unit, in the directories its readers look in. */
102
+ function resultFilesOf(projectRoot: string, unitDir: string, unit: string): string[] {
103
+ const dirs = [...new Set([...generatedDirs(projectRoot, unit), path.join(projectRoot, 'specs', 'generated', catalogIdFromScreenDir(unitDir))])];
104
+ return dirs.flatMap((d) => { try { return fs.readdirSync(d).filter((f) => RESULT_FILE.test(f)).sort().map((f) => path.join(d, f)); } catch { return []; } });
105
+ }
106
+
107
+ /** The unit's results, dated, and what they are older than — undefined when the unit has none. */
108
+ export function resultsFreshness(projectRoot: string, unitDir: string, unit: string, env: string | undefined = process.env.SUNGEN_ENV): ResultsFreshness | undefined {
109
+ const results = resultFilesOf(projectRoot, unitDir, unit);
110
+ if (!results.length) return undefined;
111
+ const features = featureFilesFor(unitDir, unit).filter((f) => fs.existsSync(f));
112
+ const specsOf = (base: string): string[] => [...new Set(results.map((r) => path.join(path.dirname(r), `${base}.spec.ts`)))].filter((f) => fs.existsSync(f));
113
+ const unitWide = [path.join(unitDir, 'requirements', 'spec.md')].filter((f) => fs.existsSync(f));
114
+ const baseOf = (f: string): string => path.basename(f).replace(RESULT_FILE, '');
115
+ const bases = [...new Set(results.map(baseOf))];
116
+ const changed = changeTimes(projectRoot, [...results, ...features, ...bases.flatMap(specsOf), ...unitWide]);
117
+ const rel = (f: string): string => posix(path.relative(projectRoot, f));
118
+
119
+ const dated = results.map((file) => {
120
+ const start = runStartOf(file);
121
+ const own = changed(file)?.ms ?? 0;
122
+ return { file, base: baseOf(file), runAt: start ?? own, source: (start !== null ? 'run' : 'file') as 'run' | 'file', recordedAt: Math.max(start ?? 0, own) };
123
+ });
124
+ const oldest = dated.reduce((a, b) => (b.runAt < a.runAt ? b : a));
125
+ const newest = dated.reduce((a, b) => (b.runAt > a.runAt ? b : a));
126
+ const recordedFor = (files: typeof dated): number => Math.min(...files.map((d) => d.recordedAt));
127
+
128
+ const newerInputs: ResultsFreshness['newerInputs'] = [];
129
+ const judge = (input: string, recordedAt: number): void => {
130
+ const c = changed(input);
131
+ if (c && c.ms > recordedAt + TIE_MS) newerInputs.push({ file: rel(input), changedAt: iso(c.ms), source: c.source });
132
+ };
133
+ for (const base of bases) {
134
+ const feeds = recordedFor(dated.filter((d) => d.base === base));
135
+ for (const f of [...features.filter((x) => path.basename(x, '.feature') === base), ...specsOf(base)]) judge(f, feeds);
136
+ }
137
+ for (const f of unitWide) judge(f, recordedFor(dated));
138
+
139
+ const variants = results.map((f) => path.basename(f).replace(/^.*-test-result\.?/, '').replace(/\.?json$/, ''));
140
+ const missingEnv = env && !variants.some((v) => v === env || v.endsWith(`.${env}`)) ? env : undefined;
141
+ const problems = [
142
+ ...(newerInputs.length ? [`results are older than ${newerInputs.slice(0, 3).map((i) => `${i.file} (changed ${shortDate(i.changedAt)})`).join(', ')}${newerInputs.length > 3 ? ', …' : ''} — re-run the tests before trusting the counts`] : []),
143
+ ...(missingEnv ? [`no results for SUNGEN_ENV=${missingEnv} — readers fall back to ${rel(oldest.file)}`] : []),
144
+ ];
145
+ return {
146
+ files: dated.map((d) => ({ file: rel(d.file), runAt: iso(d.runAt), source: d.source })),
147
+ oldestRunAt: iso(oldest.runAt), newestRunAt: iso(newest.runAt), runAtSource: oldest.source,
148
+ newerInputs, ...(missingEnv ? { missingEnv } : {}), ...(problems.length ? { warning: problems.join('; ') } : {}),
149
+ };
150
+ }
151
+
152
+ /** `2026-08-21 03:42Z` */
153
+ export const shortDate = (isoTime: string): string => `${isoTime.slice(0, 16).replace('T', ' ')}Z`;
154
+
155
+ /** `run 2026-08-21 03:42Z (65 days ago)` — the oldest run, and the newest when they are days apart. */
156
+ export function describeResultsRun(f: ResultsFreshness, now = Date.now()): string {
157
+ // A file neither the JSON nor the file system could date reads as epoch 0 — "20000 days ago" is noise.
158
+ if (!(Date.parse(f.oldestRunAt) > 0)) return 'run time unknown (no run start in the JSON, no file time)';
159
+ const days = Math.floor((now - Date.parse(f.oldestRunAt)) / 86_400_000);
160
+ const age = days <= 0 ? 'today' : days === 1 ? '1 day ago' : `${days} days ago`;
161
+ const span = Date.parse(f.newestRunAt) - Date.parse(f.oldestRunAt) >= 86_400_000 ? ` to ${shortDate(f.newestRunAt)}` : '';
162
+ return f.runAtSource === 'run'
163
+ ? `run ${shortDate(f.oldestRunAt)} (${age})${span}`
164
+ : `file time ${shortDate(f.oldestRunAt)} (${age}; the JSON records no run start)${span}`;
165
+ }
@@ -11,7 +11,7 @@ import * as fs from 'fs';
11
11
  import { loadUnitScenarios, readUnitFeatureText, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
12
12
  import { checkViewpointBaseline, ViewpointBaseline } from './viewpoint-baseline';
13
13
  import { flowQuality, statefulDepthFor, continuityGaps, flowInventory, misfiledPhases, FlowQualityResult, InventoryResult } from './flow-contract';
14
- import { featureFilesFor } from './unit-paths';
14
+ import { catalogIdFromScreenDir, featureFilesFor, isScreenUnitId } from './unit-paths';
15
15
  import {
16
16
  loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, scenarioIdReuse, ReusedScenarioId, traceability, claimProof, taxonomyLint,
17
17
  automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength, declaredPageType,
@@ -129,17 +129,8 @@ export function projectUnitDirs(projectRoot: string): string[] {
129
129
  return out;
130
130
  }
131
131
 
132
- /** The catalog-resolution id for a unit dir (relative to qa/): screen · flows/<flow> · api/<area> · api/flows/<flow>. */
133
- export function catalogIdFromScreenDir(screenDir: string): string {
134
- const parts = screenDir.split(path.sep);
135
- const qa = parts.lastIndexOf('qa');
136
- if (qa >= 0) {
137
- if (parts[qa + 1] === 'api' && parts[qa + 2] === 'flows' && parts[qa + 3]) return `api/flows/${parts[qa + 3]}`;
138
- if (parts[qa + 1] === 'api' && parts[qa + 2]) return `api/${parts[qa + 2]}`;
139
- if (parts[qa + 1] === 'flows' && parts[qa + 2]) return `flows/${parts[qa + 2]}`;
140
- }
141
- return path.basename(screenDir);
142
- }
132
+ /** The catalog-resolution id for a unit dir — lives in unit-paths.ts (the inputsHash reads it too); kept exported here. */
133
+ export { catalogIdFromScreenDir };
143
134
 
144
135
  /**
145
136
  * The capability that owns SCORING for a unit (AO-1): the first path segment of the unit id when it
@@ -246,7 +237,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
246
237
  // gate gap, credit it to the flow instead of double-counting it as a screen gap. Screens only
247
238
  // (a flow/api unit is not credited by sibling flows); mutates the gate before coverage is scored.
248
239
  const flowCredits: { theme: string; flow: string }[] = [];
249
- const isScreenUnit = !/^(flows|api)\//.test(catalogScreenName);
240
+ const isScreenUnit = isScreenUnitId(catalogScreenName);
250
241
  if (isScreenUnit && gate.gaps.length) {
251
242
  const flowScenarios = loadFlowScenarios(projectRootFromScreenDir(screenDir));
252
243
  if (flowScenarios.length) {
@@ -535,8 +535,8 @@ rules:
535
535
  stage: deliver
536
536
  severity: error
537
537
  kind: soft
538
- rationale: "Pre-delivery halt: everything automated plus the single human sign-off must be satisfied or waived with a reason."
539
- fix: "Close the open obligations and sign off, or waive with a reason."
538
+ rationale: "Pre-delivery halt: everything automated must be satisfied or waived with a reason, plus the single human sign-off, which is given (sungen journey --signoff), never waived."
539
+ fix: "Close or waive the open obligations, then a human signs off: sungen journey --screen <unit> --signoff --by <name>."
540
540
  since: "<=3.2.34"
541
541
  offAllowed: false
542
542
 
@@ -586,7 +586,7 @@ rules:
586
586
  severity: review
587
587
  kind: hard
588
588
  rationale: "A check on a JS value, API or canvas result is not tied to an element and cannot be compared automatically (DOS-14.6); it always goes to review and is locked at review."
589
- fix: "QA reviews the verbatim step and its file:line, then approves."
589
+ fix: "QA reviews the verbatim step and its file:line (`sungen contract check`), then accepts it as it reads now on a terminal: `sungen contract approve --screen <unit> --scenario <id> --accept-opaque`. A plain re-approve never clears it."
590
590
  since: "p0"
591
591
  offAllowed: false
592
592
  - id: contract.nondeterministic
@@ -20,6 +20,7 @@ import { readCapabilities } from './capability';
20
20
  import { defectsDir, nextDefectId, readDefects } from './defects';
21
21
  import type { TriageRow } from './triage';
22
22
  import { readTriageReport, unitSecrets } from './triage-unit';
23
+ import { testCaseStates } from '../graph/test-case-states';
23
24
 
24
25
  export interface DraftResult {
25
26
  /** `sungen triage` has not run for this unit. */
@@ -89,14 +90,24 @@ function render(cwd: string, unit: ResolvedUnit, id: string, row: TriageRow, sc:
89
90
  return redactSecrets(body, sc?.secrets ?? []);
90
91
  }
91
92
 
93
+ /**
94
+ * The class a triage row stands for now: a classification by hand in force for its case wins (#928) —
95
+ * a red classified `product` by hand is drafted, one a human classified test-defect is not.
96
+ */
97
+ function effectiveClass(cwd: string, unit: ResolvedUnit): (row: TriageRow) => TriageRow['class'] {
98
+ const byHand = testCaseStates(cwd, unit.unitId, { unitDir: unit.dir, auditFindings: [] }).filter((c) => c.triage?.ruleId === 'manual');
99
+ return (row) => byHand.find((c) => row.scenario === c.title || row.scenario.startsWith(`${c.title} — `))?.triage?.class ?? row.class;
100
+ }
101
+
92
102
  export function draftDefects(cwd: string, unit: ResolvedUnit, opts: { scenario?: string } = {}): DraftResult {
93
103
  const triage = readTriageReport(cwd, unit.unitId);
94
104
  if (!triage) return { missingTriage: true, written: [], skipped: [] };
95
105
  const recorded = new Map<string, string>();
96
106
  for (const r of readDefects(cwd)) if (r.units.includes(unit.unitId)) for (const s of r.scenarios) recorded.set(s, r.id);
97
107
 
98
- const wanted = triage.rows.filter((r) => r.class === 'product-candidate'
99
- && (!opts.scenario || r.scenario.includes(opts.scenario) || r.ids.includes(opts.scenario)));
108
+ const classOf = effectiveClass(cwd, unit);
109
+ const wanted = triage.rows.map((r): TriageRow => (classOf(r) === r.class ? r : { ...r, class: classOf(r), ruleId: 'manual' }))
110
+ .filter((r) => r.class === 'product-candidate' && (!opts.scenario || r.scenario.includes(opts.scenario) || r.ids.includes(opts.scenario)));
100
111
  const scenarios = unitScenarios(cwd, unit);
101
112
  const result: DraftResult = { written: [], skipped: [] };
102
113
  const seen = new Set<string>();
@@ -66,24 +66,37 @@ export function readDefects(cwd: string): DefectRecord[] {
66
66
  return out;
67
67
  }
68
68
 
69
+ /**
70
+ * Everything the defect-link findings of one unit read off qa/defects/ — and so what the audit's
71
+ * inputsHash covers there (#927): the unit's `@known-defect:<id>` tags with no record, in tag order,
72
+ * and the unit's OPEN records with the scenarios they name. Title, severity and prose are not read,
73
+ * so editing them never asks for a re-audit.
74
+ */
75
+ export interface DefectLinkInputs { missing: string[]; open: Array<{ id: string; scenarios: string[] }> }
76
+
77
+ export function defectLinkInputs(cwd: string, unitId: string, featureText: string): DefectLinkInputs {
78
+ const records = readDefects(cwd);
79
+ const known = new Set(records.map((r) => r.id));
80
+ const tagged = [...new Set([...featureText.matchAll(KNOWN_DEFECT_TAG)].map((m) => m[1]))];
81
+ return {
82
+ missing: tagged.filter((id) => !known.has(id)),
83
+ open: records.filter((r) => r.status === 'open' && r.units.includes(unitId)).map((r) => ({ id: r.id, scenarios: r.scenarios })),
84
+ };
85
+ }
86
+
69
87
  /**
70
88
  * Advisory audit findings for one unit (never scored):
71
89
  * DEFECT-REF-MISSING a `@known-defect:DEF-NNN` tag with no record file.
72
90
  * DEFECT-ORPHAN (info) an OPEN record of this unit names a scenario the unit no longer has.
73
91
  */
74
92
  export function defectLinkFindings(cwd: string, unitId: string, featureText: string, scenarioNames: string[]): string[] {
75
- const records = readDefects(cwd);
93
+ const { missing, open } = defectLinkInputs(cwd, unitId, featureText);
76
94
  const findings: string[] = [];
77
- const known = new Set(records.map((r) => r.id));
78
- const tagged = new Set([...featureText.matchAll(KNOWN_DEFECT_TAG)].map((m) => m[1]));
79
- for (const id of tagged) {
80
- if (!known.has(id)) {
81
- findings.push(`DEFECT-REF-MISSING: \`@known-defect:${id}\` has no record in qa/defects/ — draft it with \`sungen defect draft --screen ${unitId}\`, or correct the id.`);
82
- }
95
+ for (const id of missing) {
96
+ findings.push(`DEFECT-REF-MISSING: \`@known-defect:${id}\` has no record in qa/defects/ — draft it with \`sungen defect draft --screen ${unitId}\`, or correct the id.`);
83
97
  }
84
98
  const names = new Set(scenarioNames);
85
- for (const r of records) {
86
- if (r.status !== 'open' || !r.units.includes(unitId)) continue;
99
+ for (const r of open) {
87
100
  for (const s of r.scenarios) {
88
101
  if (!names.has(s)) {
89
102
  findings.push(`DEFECT-ORPHAN (info): ${r.id} is open and names scenario "${s}", which ${unitId} no longer has — update the record's scenarios, or close it.`);
@@ -70,6 +70,17 @@ export function loadFlowScenarios(cwd: string): FlowScenario[] {
70
70
  return out;
71
71
  }
72
72
 
73
+ /**
74
+ * What a screen audit reads from its sibling flows (#927): the scenarios that can credit a gate theme
75
+ * — only deep ones do (flowCoveredThemes) — by flow and haystack. The screen's inputsHash covers them,
76
+ * sorted so the hash does not depend on directory order; a shallow scenario or prose moves nothing.
77
+ */
78
+ export function flowCreditInputs(cwd: string): Array<{ flow: string; haystack: string }> {
79
+ const key = (s: { flow: string; haystack: string }): string => `${s.flow}\n${s.haystack}`;
80
+ return loadFlowScenarios(cwd).filter((s) => s.deep).map(({ flow, haystack }) => ({ flow, haystack }))
81
+ .sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
82
+ }
83
+
73
84
  export function buildFlowCheck(cwd: string, onlyFlow?: string): FlowCheckReport {
74
85
  const screens = listDirs(path.join(cwd, 'qa', 'screens'));
75
86
  const flows = (onlyFlow ? [onlyFlow] : listDirs(path.join(cwd, 'qa', 'flows')));
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * gateOverrides[] every override touching the unit, applied / ineffective / rejected
7
7
  * decisionsHash the overrides it was judged under (stale when they change)
8
- * inputsHash .feature + spec + viewpoint + decisionsHash
8
+ * inputsHash .feature + spec + viewpoint + … + decisionsHash, and the defect records and
9
+ * crediting flows it reads (audit-input-hash.ts, #927)
9
10
  * exitCode 0 on PASS, 2 on FAIL — what `sungen audit` exits with
10
11
  *
11
12
  * The decision layer adds three hard terms: a rejected override (gate.override-rejected) and a
@@ -12,14 +12,23 @@
12
12
  * requirements/flow-contract.yaml · requirements/spec_figma.md · requirements/ui-inventory.md ·
13
13
  * requirements/ui/* · qa/context.md (intent focus → thresholds) · qa/capabilities.yaml (platform,
14
14
  * enabled drivers → gate sensors)
15
- * Not yet (phase 05 follow-ups): qa/defects/ and the sibling flows credited to a screen.
15
+ * and what it reads outside the unit (#927), as the audit reads it — not the files whole, so a
16
+ * re-audit is asked for only when the verdict can move:
17
+ * defects qa/defects/: the unit's `@known-defect:<id>` tags with no record, and its OPEN records
18
+ * with their scenarios (defectLinkInputs — DEFECT-REF-MISSING, DEFECT-ORPHAN)
19
+ * flowCredit a SCREEN's sibling flows: the deep scenarios that can credit a gate theme
20
+ * (flowCreditInputs — TQ-4, COVERED-VIA-FLOW); a flow or api unit is never credited
21
+ * Each is part of the hash only when it holds something, so a project with neither keeps the hash it
22
+ * had; a report written before #927 in a project with either reads stale once.
16
23
  */
17
24
  import { createHash } from 'crypto';
18
25
  import * as fs from 'fs';
19
26
  import * as path from 'path';
20
27
  import { canonicalJson } from '../../decisions/decision-identity';
28
+ import { defectLinkInputs } from '../defects';
29
+ import { flowCreditInputs } from '../flow-check';
21
30
  import { projectRootFromScreenDir } from '../intent';
22
- import { featureFilesFor } from '../unit-paths';
31
+ import { catalogIdFromScreenDir, featureFilesFor, isScreenUnitId } from '../unit-paths';
23
32
 
24
33
  const sha256 = (s: string | Buffer): string => createHash('sha256').update(s).digest('hex');
25
34
 
@@ -47,9 +56,24 @@ export function auditInputFiles(unitDir: string, unitName: string): string[] {
47
56
  return candidates.filter((f) => fs.existsSync(f));
48
57
  }
49
58
 
59
+ /** What the audit reads outside the unit — each key only when it holds something. */
60
+ function crossUnitInputs(root: string, unitDir: string, unitName: string): Record<string, unknown> {
61
+ const unitId = catalogIdFromScreenDir(unitDir);
62
+ const featureText = featureFilesFor(unitDir, unitName).map((f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return ''; } }).join('\n');
63
+ const defects = defectLinkInputs(root, unitId, featureText);
64
+ const flowCredit = isScreenUnitId(unitId) ? flowCreditInputs(root) : [];
65
+ return { ...(defects.missing.length || defects.open.length ? { defects } : {}), ...(flowCredit.length ? { flowCredit } : {}) };
66
+ }
67
+
50
68
  export function auditInputsHash(unitDir: string, unitName: string, decisionsHash: string): string {
51
69
  const root = projectRootFromScreenDir(unitDir);
52
70
  const inputs = auditInputFiles(unitDir, unitName)
53
71
  .map((f) => ({ file: path.relative(root, f).split(path.sep).join('/'), sha: fileSha(f) }));
54
- return sha256(canonicalJson({ schema: 2, inputs, decisionsHash }));
72
+ return sha256(canonicalJson({ schema: 2, inputs, decisionsHash, ...crossUnitInputs(root, unitDir, unitName) }));
73
+ }
74
+
75
+ /** The stale reason when the inputsHash moved — it names every kind of input, the ones outside the unit too. */
76
+ export function auditInputsChangedReason(unitDir: string): string {
77
+ const outside = isScreenUnitId(catalogIdFromScreenDir(unitDir)) ? 'the defect records and sibling flows it reads' : 'the defect records it reads';
78
+ return `an input the audit judged (.feature, spec, viewpoint, context, design, or ${outside}) changed since this audit`;
55
79
  }