@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
@@ -8,13 +8,21 @@
8
8
  * refused the trace exists but may not speak (swapped, stale, rejected zip…)
9
9
  * unsupported the trace's format version is unknown (exit ≠ 0)
10
10
  *
11
+ * Two recorded TESTS claiming one scenario id (#962) make its evidence ambiguous: neither speaks —
12
+ * `unproven`, both named (scenario-id-claims.ts) — instead of judging two tests' runs as one.
13
+ *
11
14
  * Only traces of the scenario ids asked for are opened, so a unit pays for its own tests only.
12
15
  */
13
16
  import * as fs from 'fs';
14
17
  import * as path from 'path';
18
+ import { isIdLike } from '../../harness/parse';
19
+ import { clip, CONTROL_CHARS } from '../render-free-gherkin';
20
+ import { FileRead, readInsideProject } from './read-file-once';
21
+ import { idsClaimedByTwoTests, twoTestsClaim } from './scenario-id-claims';
15
22
  import { bindTrace } from './trace-binding';
16
- import { readTraceFile, ParsedTrace } from './trace-file-reader';
17
- import { TraceRejected } from './trace-zip-limits';
23
+ import type { UnitContract } from './trace-freshness';
24
+ import { traceFromRead, ParsedTrace } from './trace-file-reader';
25
+ import { traceZipLimits, TraceRejected } from './trace-zip-limits';
18
26
  import { finalAttempt, readPlaywrightReport, ReportResult, ReportTest, scenarioIdOfTest } from './select-final-attempt';
19
27
  import { findRunRecords, readRunRecord, RunRecord } from './trace-run-record';
20
28
 
@@ -32,78 +40,139 @@ export interface TraceRun {
32
40
  }
33
41
  export interface ScenarioEvidence { runs: Map<string, TraceRun[]>; problems: string[]; inputs: string[] }
34
42
 
43
+ /** A run record or JSON report larger than this is refused before a byte is read (fail closed, #926). */
44
+ export const MAX_EVIDENCE_JSON_BYTES = 64 * 1024 * 1024;
45
+
46
+ /** Why an evidence file was not read — `outside the project` covers a link out, a swap and a non-file. */
47
+ function notRead(name: string, r: Exclude<FileRead, { status: 'read' }>): string {
48
+ if (r.status === 'too-large') return `${name} is larger than ${MAX_EVIDENCE_JSON_BYTES / 1024 / 1024} MiB — not read`;
49
+ if (r.status === 'missing') return `${name} is missing — not read`;
50
+ return `${name} is outside the project (a symlink?) or not a regular file — not read`;
51
+ }
52
+
35
53
  /** The reason a run is not judged once the verdict's deadline has passed (fail closed). */
36
54
  export const OVER_BUDGET = 'verdict budget exceeded; run `sungen contract check`';
37
- const NO_RECORD = "no sungen run record under test-results/ — run the free tests with trace: 'on' and the sungen run-record reporter";
55
+ const NO_RECORD = "no sungen run record under test-results-free/ (or test-results/) — run the free tests with their own config: outputDir 'test-results-free', trace: 'on' and the sungen run-record reporter";
38
56
 
39
- /** The real path of `file` when it is a regular file INSIDE the project (symlinks resolved); else null. */
40
- export function insideProject(projectRoot: string, file: string): string | null {
41
- try {
42
- const root = fs.realpathSync(projectRoot);
43
- const real = fs.realpathSync(file);
44
- return real.startsWith(root + path.sep) && fs.statSync(real).isFile() ? real : null;
45
- } catch { return null; }
57
+ /** `p` relative to `root` when it lies under it, else null. */
58
+ function under(root: string, p: string): string | null {
59
+ const rel = path.relative(root, p);
60
+ return rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : null;
46
61
  }
47
62
 
48
- function resolveAttachment(projectRoot: string, report: string, p: string): { file?: string; outside?: boolean } {
49
- for (const cand of path.isAbsolute(p) ? [p] : [path.resolve(projectRoot, p), path.resolve(path.dirname(report), p)]) {
50
- if (!fs.existsSync(cand)) continue;
51
- const real = insideProject(projectRoot, cand);
52
- return real ? { file: real } : { outside: true };
53
- }
54
- return {};
63
+ /**
64
+ * Where a report's trace attachment is: the first candidate that exists (read later, opened once, and
65
+ * only from inside the project). Playwright writes attachment paths ABSOLUTE, in the checkout that ran;
66
+ * after a clone or a move that checkout is elsewhere, so a path under the report's own config dir is
67
+ * first tried at the same place under this project (#967) — the run record's sha256 still binds the bytes.
68
+ */
69
+ function attachmentPath(projectRoot: string, test: ReportTest, p: string): string | null {
70
+ const moved = path.isAbsolute(p) && test.configRoot && !under(path.resolve(projectRoot), p) ? under(test.configRoot, p) : null;
71
+ const candidates = path.isAbsolute(p)
72
+ ? [...(moved ? [path.resolve(projectRoot, moved)] : []), p]
73
+ : [path.resolve(projectRoot, p), path.resolve(path.dirname(test.report), p)];
74
+ return candidates.find((c) => fs.existsSync(c)) ?? null;
55
75
  }
56
76
 
57
- function judge(projectRoot: string, record: RunRecord, test: ReportTest, notBefore: number): TraceRun {
77
+ function judge(projectRoot: string, record: RunRecord, test: ReportTest, contract: UnitContract | null): TraceRun {
58
78
  const scenarioId = scenarioIdOfTest(test.title, test.tags);
59
79
  const base = { scenarioId, test, record };
60
80
  const fin = finalAttempt(test);
61
81
  if (fin.admissibility !== 'admissible') return { ...base, result: fin.result, status: fin.admissibility, reason: fin.reason };
62
- const { file, outside } = resolveAttachment(projectRoot, test.report, fin.result!.tracePath!);
63
- if (outside) return { ...base, result: fin.result, status: 'unproven', reason: `the trace ${path.basename(fin.result!.tracePath!)} is outside the project (or not a file) — not read` };
64
- if (!file) return { ...base, result: fin.result, status: 'unproven', reason: `no trace at ${fin.result!.tracePath} (the report points at a missing file)` };
82
+ const missing: TraceRun = { ...base, result: fin.result, status: 'unproven', reason: `no trace at ${fin.result!.tracePath} (the report points at a missing file)` };
83
+ const cand = attachmentPath(projectRoot, test, fin.result!.tracePath!);
84
+ if (!cand) return missing;
85
+ // Opened once, inside the project, and what was opened is what is parsed and hashed (#926).
86
+ const limits = traceZipLimits();
87
+ const read = readInsideProject(projectRoot, cand, limits.maxZipBytes);
88
+ if (read.status === 'missing') return missing;
89
+ if (read.status === 'outside' || read.status === 'not-a-file') {
90
+ return { ...base, result: fin.result, status: 'unproven', reason: `the trace ${path.basename(fin.result!.tracePath!)} is outside the project (or not a file) — not read` };
91
+ }
65
92
  let trace: ParsedTrace;
66
- try { trace = readTraceFile(file); } catch (e) {
93
+ try { trace = traceFromRead(read, cand, limits); } catch (e) {
67
94
  const unsupported = e instanceof TraceRejected && e.code === 'trace.version-unsupported';
68
95
  return { ...base, result: fin.result, status: unsupported ? 'unsupported' : 'refused', reason: `trace refused: ${(e as Error).message}` };
69
96
  }
70
- const bound = bindTrace({ projectRoot, test, result: fin.result!, record, trace, notBefore });
97
+ const bound = bindTrace({ projectRoot, test, result: fin.result!, record, trace, contract });
71
98
  if (bound.ok === false) return { ...base, result: fin.result, trace, status: 'refused', reason: `trace refused: ${bound.reason}` };
72
99
  return { ...base, result: fin.result, trace, status: 'admissible', specFile: bound.specFile };
73
100
  }
74
101
 
75
- /** Every judged run of the tests `want` accepts (derive: a unit's tests; check: by scenario id). */
102
+ /**
103
+ * Every judged run of the tests `want` accepts (derive: a unit's tests; check: by scenario id). A trace
104
+ * older than `contract` — the unit's .feature files and lock, by content (#967) — is refused; derive,
105
+ * which writes the contract from the traces, passes null.
106
+ */
76
107
  export function collectRuns(
77
- projectRoot: string, want: (id: string, test: ReportTest) => boolean, notBefore: number, deadline = Number.POSITIVE_INFINITY,
78
- ): { runs: TraceRun[]; problems: string[]; inputs: string[]; records: number } {
108
+ projectRoot: string, want: (id: string, test: ReportTest) => boolean, contract: UnitContract | null, deadline = Number.POSITIVE_INFINITY,
109
+ ): { runs: TraceRun[]; problems: string[]; untrusted: string[]; inputs: string[]; records: number; seen: Array<{ id: string; title: string }> } {
79
110
  const runs: TraceRun[] = [];
80
111
  const problems: string[] = [];
112
+ const untrusted: string[] = [];
81
113
  const inputs: string[] = [];
114
+ const seen: Array<{ id: string; title: string }> = [];
82
115
  const records = findRunRecords(projectRoot);
83
116
  for (const file of records) {
84
117
  let record: RunRecord;
85
- if (!insideProject(projectRoot, file)) { problems.push(`${path.basename(file)} is outside the project (a symlink?) — not read`); continue; }
86
- try { record = readRunRecord(file); } catch (e) { problems.push(`${path.basename(file)} is not trusted: ${(e as Error).message}`); continue; }
118
+ const recordRead = readInsideProject(projectRoot, file, MAX_EVIDENCE_JSON_BYTES);
119
+ if (recordRead.status !== 'read') { problems.push(notRead(path.basename(file), recordRead)); continue; }
120
+ try { record = readRunRecord(file, recordRead.bytes.toString('utf8')); } catch (e) {
121
+ untrusted.push(`${path.basename(file)} is not trusted: ${(e as Error).message}`);
122
+ problems.push(untrusted[untrusted.length - 1]);
123
+ continue;
124
+ }
87
125
  if (!record.report) { problems.push(`${path.basename(file)} names no JSON report — add the json reporter to the free-test config`); continue; }
88
- const reportFile = insideProject(projectRoot, path.resolve(projectRoot, record.report));
89
- if (!reportFile) { problems.push(`${path.basename(file)}: the report ${path.basename(record.report)} is missing or outside the project — not read`); continue; }
126
+ const reportRead = readInsideProject(projectRoot, path.resolve(projectRoot, record.report), MAX_EVIDENCE_JSON_BYTES);
127
+ if (reportRead.status !== 'read') { problems.push(`${path.basename(file)}: ${notRead(`the report ${path.basename(record.report)}`, reportRead)}`); continue; }
90
128
  let tests: ReportTest[];
91
- try { tests = readPlaywrightReport(reportFile); } catch (e) { problems.push((e as Error).message); continue; }
129
+ try { tests = readPlaywrightReport(reportRead.real, reportRead.bytes.toString('utf8')); } catch (e) { problems.push((e as Error).message); continue; }
92
130
  inputs.push(`${record.runId}:${record.report}`);
93
131
  for (const test of tests) {
94
132
  const id = scenarioIdOfTest(test.title, test.tags);
133
+ seen.push({ id, title: test.title });
95
134
  if (!want(id, test)) continue;
96
- runs.push(Date.now() > deadline ? { scenarioId: id, test, record, status: 'unproven', reason: OVER_BUDGET } : judge(projectRoot, record, test, notBefore));
135
+ runs.push(Date.now() > deadline ? { scenarioId: id, test, record, status: 'unproven', reason: OVER_BUDGET } : judge(projectRoot, record, test, contract));
97
136
  }
98
137
  }
99
- return { runs, problems, inputs, records: records.length };
138
+ return { runs, problems, untrusted, inputs, records: records.length, seen };
139
+ }
140
+
141
+ /** A title without its leading id, for comparing words only: `VP-X-001 Login works` → `login works`. */
142
+ const wordsOf = (title: string): string => {
143
+ const lead = title.match(/^\s*([A-Za-z][A-Za-z0-9.-]*)/)?.[1];
144
+ return (lead && isIdLike(lead) ? title.trim().slice(lead.length) : title).replace(CONTROL_CHARS, '').replace(/\s+/g, ' ').trim().toLowerCase();
145
+ };
146
+
147
+ /**
148
+ * Why no recorded test speaks for `id` (#943): a recorded test with this scenario's title under
149
+ * ANOTHER id is the usual cause — the id was renamed in the .free.feature (or in the test) and the
150
+ * other side was not — so that mismatch is named, with the fix.
151
+ */
152
+ function noTestFor(id: string, name: string | undefined, seen: Array<{ id: string; title: string }>): string {
153
+ const words = name ? wordsOf(name) : '';
154
+ const twin = words ? seen.find((t) => t.id !== id && wordsOf(t.title) === words) : undefined;
155
+ if (!twin) return `no test for ${id} in the recorded run(s)`;
156
+ // The title and its id come from the report — untrusted: clipped, control characters dropped.
157
+ const [title, other] = [clip(twin.title), clip(twin.id, 60)];
158
+ return `no test for ${id} in the recorded run(s) — the recorded test "${title}" (id ${other}) has this scenario's title, so the scenario id and the test no longer match: `
159
+ + `give the test the id ${id} (in its title and its @tag), or the scenario the id ${other}, then run it again with trace on`;
100
160
  }
101
161
 
102
- export function collectScenarioEvidence(projectRoot: string, ids: ReadonlySet<string>, notBefore: number, deadline?: number): ScenarioEvidence {
103
- const found = collectRuns(projectRoot, (id) => ids.has(id), notBefore, deadline);
162
+ /** The runs of each scenario id; `names` (id → scenario name) lets a missing test be explained. */
163
+ export function collectScenarioEvidence(
164
+ projectRoot: string, ids: ReadonlySet<string>, contract: UnitContract | null, deadline?: number, names?: ReadonlyMap<string, string>,
165
+ ): ScenarioEvidence {
166
+ const found = collectRuns(projectRoot, (id) => ids.has(id), contract, deadline);
104
167
  const runs = new Map<string, TraceRun[]>([...ids].map((id) => [id, found.runs.filter((r) => r.scenarioId === id)]));
168
+ // Ambiguous evidence: no run of either test may speak for the id — never one picked arbitrarily.
169
+ for (const [id, claim] of idsClaimedByTwoTests(projectRoot, found.runs)) {
170
+ if (runs.has(id)) runs.set(id, [{ scenarioId: id, status: 'unproven', reason: twoTestsClaim(id, claim) }]);
171
+ }
105
172
  for (const [id, list] of runs) {
106
- if (!list.length) list.push({ scenarioId: id, status: 'unproven', reason: found.records ? `no test for ${id} in the recorded run(s)` : NO_RECORD });
173
+ // A record that was not trusted may be why (a newer reporter, a hand edit): the finding says so.
174
+ const why = found.untrusted.length ? ` — ${found.untrusted.join('; ')}` : '';
175
+ if (!list.length) list.push({ scenarioId: id, status: 'unproven', reason: found.records ? `${noTestFor(id, names?.get(id), found.seen)}${why}` : NO_RECORD });
107
176
  }
108
177
  return { runs, problems: found.problems, inputs: found.inputs };
109
178
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Read trace evidence (a run record, a JSON report, a trace.zip) by opening it ONCE and checking what
3
+ * was opened (#926) — never "check the path, then read the path", which leaves a window in which the
4
+ * file can be swapped for a link to anything outside the project.
5
+ *
6
+ * readFileOnce(file) a regular file, read from its descriptor; O_NONBLOCK so a
7
+ * FIFO swapped in cannot hang the guard; size checked on the
8
+ * descriptor before a byte is read
9
+ * readInsideProject(root, file) the same for a path from untrusted input:
10
+ * 1. its real path must sit inside the project's real root
11
+ * 2. that real path is opened with O_NOFOLLOW (a link put
12
+ * there in the meantime fails the open)
13
+ * 3. AFTER the open the path is resolved again: still inside,
14
+ * and the same file (device + inode) as the descriptor —
15
+ * a swap made before the open and undone after it is caught
16
+ *
17
+ * A file truncated or grown while it is read gives the bytes the descriptor had; the caller hashes
18
+ * those bytes, so nothing unread is ever vouched for.
19
+ *
20
+ * Windows has no O_NOFOLLOW (the constant is 0 there): the open follows a link swapped in, and the
21
+ * device + inode re-check after the open is what refuses it.
22
+ */
23
+ import * as fs from 'fs';
24
+ import * as path from 'path';
25
+
26
+ export type FileRead =
27
+ | { status: 'read'; real: string; bytes: Buffer }
28
+ | { status: 'missing' }
29
+ | { status: 'outside'; why: string }
30
+ | { status: 'not-a-file' }
31
+ | { status: 'too-large'; size: number };
32
+
33
+ const { O_RDONLY } = fs.constants;
34
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;
35
+ const O_NONBLOCK = fs.constants.O_NONBLOCK ?? 0;
36
+
37
+ function readFd(fd: number, size: number): Buffer {
38
+ const buf = Buffer.alloc(size);
39
+ let off = 0;
40
+ while (off < size) {
41
+ const n = fs.readSync(fd, buf, off, size - off, off);
42
+ if (n === 0) break;
43
+ off += n;
44
+ }
45
+ return off === size ? buf : buf.subarray(0, off);
46
+ }
47
+
48
+ function openAndRead(file: string, flags: number, maxBytes: number, recheck?: (opened: fs.Stats) => string | null): FileRead {
49
+ let fd: number;
50
+ try { fd = fs.openSync(file, flags); } catch (e) {
51
+ const code = (e as NodeJS.ErrnoException).code;
52
+ if (code === 'ENOENT') return { status: 'missing' };
53
+ // O_NOFOLLOW on a link: ELOOP (Linux, macOS) or EMLINK (FreeBSD).
54
+ if (code === 'ELOOP' || code === 'EMLINK') return { status: 'outside', why: 'it became a link while it was opened' };
55
+ throw e;
56
+ }
57
+ try {
58
+ const st = fs.fstatSync(fd);
59
+ if (!st.isFile()) return { status: 'not-a-file' };
60
+ if (st.size > maxBytes) return { status: 'too-large', size: st.size };
61
+ const moved = recheck?.(st);
62
+ if (moved) return { status: 'outside', why: moved };
63
+ return { status: 'read', real: file, bytes: readFd(fd, st.size) };
64
+ } finally {
65
+ fs.closeSync(fd);
66
+ }
67
+ }
68
+
69
+ /** A regular file, opened once and read from that descriptor. */
70
+ export const readFileOnce = (file: string, maxBytes = Number.POSITIVE_INFINITY): FileRead =>
71
+ openAndRead(file, O_RDONLY | O_NONBLOCK, maxBytes);
72
+
73
+ /** A regular file INSIDE the project (symlinks resolved), opened once; see the header for the checks. */
74
+ export function readInsideProject(projectRoot: string, file: string, maxBytes = Number.POSITIVE_INFINITY): FileRead {
75
+ let root: string;
76
+ let real: string;
77
+ try {
78
+ root = fs.realpathSync(projectRoot);
79
+ real = fs.realpathSync(file);
80
+ } catch (e) {
81
+ return (e as NodeJS.ErrnoException).code === 'ENOENT' ? { status: 'missing' } : { status: 'outside', why: 'its real path cannot be resolved' };
82
+ }
83
+ const within = (p: string): boolean => p.startsWith(root + path.sep);
84
+ if (!within(real)) return { status: 'outside', why: 'its real path is outside the project' };
85
+ return openAndRead(real, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, maxBytes, (opened) => {
86
+ try {
87
+ const again = fs.realpathSync(file);
88
+ if (!within(again)) return 'it was swapped for a link outside the project while it was opened';
89
+ const now = fs.statSync(again);
90
+ return now.dev === opened.dev && now.ino === opened.ino ? null : 'it was swapped for another file while it was opened';
91
+ } catch {
92
+ return 'it moved while it was opened';
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * One scenario id, one test (#962): which ids more than one recorded TEST claims, said the same way
3
+ * by `contract derive` (it refuses) and `contract check` (the id is unproven). One test recorded
4
+ * again — another run or shard, a retry, a second Playwright project — is still one test: the same
5
+ * file and the same Playwright test id.
6
+ *
7
+ * Each claiming test names the run record(s) it came from. A test that only an older run's record
8
+ * under `test-results/` still mentions — while another claim comes from the free config's own
9
+ * `test-results-free/` — is most likely gone: that record is offered for deletion.
10
+ */
11
+ import * as path from 'path';
12
+ import { clip } from '../render-free-gherkin';
13
+ import type { TraceRun } from './collect-trace-evidence';
14
+ import { FREE_RUN_DIR } from './trace-run-record';
15
+
16
+ export interface IdClaim {
17
+ /** `file:line "title" (run record <path>)` per claiming test, sorted. */
18
+ tests: string[];
19
+ /** Project-relative records under test-results/ that are a claim's only source, next to a test-results-free/ claim. */
20
+ staleRecords: string[];
21
+ }
22
+
23
+ const posix = (p: string): string => p.split(path.sep).join('/');
24
+ /** A run's project-relative test file: the run record's (what the binding checks), else the report's. */
25
+ const testFileOf = (r: TraceRun): string => r.record?.attempts.find((a) => a.testId === r.test?.testId)?.file ?? r.test?.file ?? '?';
26
+ const label = (r: TraceRun, records: string[]): string =>
27
+ `${clip(testFileOf(r), 200)}:${r.test?.line ?? 0} "${clip(r.test?.title ?? '')}" (run record ${records.map((f) => clip(f, 200)).join(', ')})`;
28
+
29
+ export function idsClaimedByTwoTests(projectRoot: string, runs: readonly TraceRun[]): Map<string, IdClaim> {
30
+ const byId = new Map<string, Map<string, { run: TraceRun; records: Set<string> }>>();
31
+ for (const r of runs) {
32
+ if (!r.test) continue;
33
+ const tests = byId.get(r.scenarioId) ?? new Map<string, { run: TraceRun; records: Set<string> }>();
34
+ const key = `${testFileOf(r)}\0${r.test.testId}`;
35
+ const entry = tests.get(key) ?? { run: r, records: new Set<string>() };
36
+ if (r.record?.file) entry.records.add(posix(path.relative(projectRoot, r.record.file)));
37
+ tests.set(key, entry);
38
+ byId.set(r.scenarioId, tests);
39
+ }
40
+ const claims = new Map<string, IdClaim>();
41
+ for (const [id, tests] of byId) {
42
+ if (tests.size < 2) continue;
43
+ const entries = [...tests.values()].map((t) => ({ ...t, records: [...t.records].sort() }));
44
+ const inFreeDir = entries.some((t) => t.records.some((f) => f.startsWith(`${FREE_RUN_DIR}/`)));
45
+ const legacyOnly = entries.filter((t) => t.records.length > 0 && t.records.every((f) => f.startsWith('test-results/')));
46
+ claims.set(id, {
47
+ tests: entries.map((t) => label(t.run, t.records)).sort(),
48
+ staleRecords: inFreeDir ? [...new Set(legacyOnly.flatMap((t) => t.records))].sort() : [],
49
+ });
50
+ }
51
+ return claims;
52
+ }
53
+
54
+ /** What to tell the user when several tests claim one id — check and derive say it the same way. */
55
+ export function twoTestsClaim(id: string, claim: IdClaim): string {
56
+ const stale = claim.staleRecords.length
57
+ ? `; if that test is gone, delete ${claim.staleRecords.join(', ')} — an older run's record (free tests now record under ${FREE_RUN_DIR}/)`
58
+ : '';
59
+ return `${claim.tests.length} recorded tests claim ${id}: ${claim.tests.join(', ')} — give each test its own ID (its title's leading ID, or its @tag), then run them again with trace on${stale}`;
60
+ }
@@ -11,10 +11,15 @@
11
11
  * leading id, else the whole title — the same rule the contract uses for a scenario name.
12
12
  */
13
13
  import * as fs from 'fs';
14
+ import * as path from 'path';
14
15
  import { isIdLike } from '../../harness/parse';
15
16
 
16
17
  export interface ReportResult { retry: number; status: string; startTime: string; duration: number; tracePath: string | null }
17
- export interface ReportTest { testId: string; title: string; file: string; line: number; tags: string[]; project: string; results: ReportResult[]; report: string }
18
+ export interface ReportTest {
19
+ testId: string; title: string; file: string; line: number; tags: string[]; project: string; results: ReportResult[]; report: string;
20
+ /** The directory of the config that ran (`config.configFile`, absolute as Playwright writes it) — where absolute attachment paths start. */
21
+ configRoot?: string;
22
+ }
18
23
  export type Admissibility = 'admissible' | 'inconclusive' | 'unproven';
19
24
 
20
25
  const isObj = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
@@ -32,12 +37,14 @@ function resultOf(r: unknown): ReportResult | null {
32
37
  return { retry: r.retry as number, status: r.status, startTime: String(r.startTime ?? ''), duration: Number(r.duration) || 0, tracePath: trace?.path ?? null };
33
38
  }
34
39
 
35
- /** Every test of a report; throws when the file is not a Playwright JSON report at all. */
36
- export function readPlaywrightReport(file: string): ReportTest[] {
40
+ /** Every test of a report (`text`: its bytes, already read once by the caller); throws when it is not a Playwright JSON report at all. */
41
+ export function readPlaywrightReport(file: string, text?: string): ReportTest[] {
37
42
  let doc: unknown;
38
- try { doc = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`the JSON report ${file} is not readable (${(e as Error).message.split('\n')[0]})`); }
43
+ try { doc = JSON.parse(text ?? fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`the JSON report ${file} is not readable (${(e as Error).message.split('\n')[0]})`); }
39
44
  if (!isObj(doc) || !Array.isArray(doc.suites)) throw new Error(`${file} is not a Playwright JSON report (no suites)`);
40
45
  const out: ReportTest[] = [];
46
+ const configFile = isObj(doc.config) && typeof doc.config.configFile === 'string' && path.isAbsolute(doc.config.configFile) ? doc.config.configFile : null;
47
+ const configRoot = configFile ? { configRoot: path.dirname(configFile) } : {};
41
48
  for (const top of doc.suites) {
42
49
  for (const spec of specsOf(top)) {
43
50
  if (typeof spec.id !== 'string' || typeof spec.title !== 'string' || typeof spec.file !== 'string') continue;
@@ -46,7 +53,7 @@ export function readPlaywrightReport(file: string): ReportTest[] {
46
53
  const results = arr(t.results).map(resultOf).filter((r): r is ReportResult => !!r).sort((a, b) => a.retry - b.retry);
47
54
  out.push({
48
55
  testId: spec.id, title: spec.title, file: spec.file, line: Number(spec.line) || 0,
49
- tags: arr(spec.tags).map(String), project: String(t.projectName ?? ''), results, report: file,
56
+ tags: arr(spec.tags).map(String), project: String(t.projectName ?? ''), results, report: file, ...configRoot,
50
57
  });
51
58
  }
52
59
  }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Join the test runner's steps (test.trace) with the browser calls (<N>-trace.trace) — phase 07
3
- * criterion 2, proven 114/114 on a real app. A library call's `stepId` is the runner step's
4
- * `callId`; walking the runner's `parentId` chain says where the call ran:
3
+ * criterion 2, proven 114/114 on a real app. Up to format 9 a library call's `stepId` is the runner
4
+ * step's `callId`; from format 10 (Playwright 1.64) the two share ONE id and `stepId` is gone, so the
5
+ * call's own `callId` is the step. Walking the runner's `parentId` chain says where the call ran:
5
6
  *
6
7
  * body the test function itself, with the path of enclosing `test.step` titles
7
8
  * hook a user `beforeEach` / `afterEach` (stage setup | teardown)
@@ -26,7 +27,7 @@ export type CallPhase =
26
27
  | { phase: 'unjoined' };
27
28
 
28
29
  export interface JoinedCall {
29
- part: string; callId: string; callNumber: number; class: string; method: string;
30
+ part: string; callId: string; class: string; method: string;
30
31
  params: Record<string, unknown>; stepId?: string; where: CallPhase; failed: boolean; startTime: number;
31
32
  }
32
33
 
@@ -90,9 +91,9 @@ export function joinCalls(trace: ParsedTrace): { steps: Map<string, RunnerStep>;
90
91
  for (const e of part.events) {
91
92
  if (e.type !== 'before') continue;
92
93
  const callId = str(e.callId);
93
- const stepId = str(e.stepId) || undefined;
94
+ const stepId = str(e.stepId) || (part.version >= 10 ? callId : '') || undefined;
94
95
  calls.push({
95
- part: part.name, callId, callNumber: Number(callId.replace(/^call@/, '')) || 0, class: str(e.class), method: str(e.method),
96
+ part: part.name, callId, class: str(e.class), method: str(e.method),
96
97
  params: (e.params && typeof e.params === 'object' ? e.params : {}) as Record<string, unknown>,
97
98
  stepId, where: stepId ? classifyStep(steps, stepId) : { phase: 'unjoined' }, failed: failed.has(callId), startTime: Number(e.startTime) || 0,
98
99
  });
@@ -8,12 +8,14 @@
8
8
  * code sha256 of the test file NOW equals the hash the record took at run start
9
9
  * title every library part says `<file>:<line> › … › <title>` of exactly this test
10
10
  * clock the trace started inside the attempt's window (±2 s)
11
- * fresh the trace is newer than the .feature and the lock it is asked to prove
11
+ * fresh the trace is not older than the .feature files and the lock it is asked to prove —
12
+ * judged by their content, not their file time (#967, trace-freshness.ts)
12
13
  */
13
14
  import { createHash } from 'crypto';
14
15
  import * as fs from 'fs';
15
16
  import * as path from 'path';
16
17
  import type { ParsedTrace } from './trace-file-reader';
18
+ import { staleReason, UnitContract } from './trace-freshness';
17
19
  import type { RunRecord } from './trace-run-record';
18
20
  import type { ReportResult, ReportTest } from './select-final-attempt';
19
21
 
@@ -23,8 +25,8 @@ export interface BindingInput {
23
25
  result: ReportResult;
24
26
  record: RunRecord;
25
27
  trace: ParsedTrace;
26
- /** Epoch ms: the newest mtime of the unit's .feature files and lock. */
27
- notBefore: number;
28
+ /** The unit's contract as it is now; null when no contract is proven (derive reads traces to write one). */
29
+ contract: UnitContract | null;
28
30
  }
29
31
  export type Binding = { ok: true; specFile: string } | { ok: false; reason: string };
30
32
 
@@ -51,6 +53,7 @@ export function bindTrace(i: BindingInput): Binding {
51
53
  if (typeof wall !== 'number' || Number.isNaN(start) || wall < start - CLOCK_SLACK_MS || wall > start + i.result.duration + CLOCK_SLACK_MS) {
52
54
  return fail('the trace was not recorded during this attempt (clock window)');
53
55
  }
54
- if (wall < i.notBefore) return fail('the trace is older than the .feature or the lock it should prove (stale) — re-run the test');
56
+ const stale = i.contract ? staleReason(i.contract, i.record) : null;
57
+ if (stale) return fail(stale);
55
58
  return { ok: true, specFile: attempt.file };
56
59
  }
@@ -5,20 +5,25 @@
5
5
  * test.trace the test runner's steps (test.step, hooks, fixtures, expect, pw:api)
6
6
  * <N>-trace.trace a library part: the browser calls, frame snapshots, logs
7
7
  * <N>-trace.network resources (stylesheets inlined into a replayed snapshot come from here)
8
- * <N>-trace.stacks call number → source frames (`file:line` of an opaque step)
8
+ * <N>-trace.stacks call id → source frames (`file:line` of an opaque step)
9
9
  * resources/<sha1> resource bodies
10
10
  *
11
11
  * Every part that has a header (test.trace and each library part) must declare a supported version;
12
12
  * a part without one is refused. Every JSONL line must parse — a line that does not is never skipped.
13
13
  */
14
14
  import { createHash } from 'crypto';
15
- import * as fs from 'fs';
15
+ import { FileRead, readFileOnce } from './read-file-once';
16
16
  import { openZip, ZipArchive } from './trace-zip-reader';
17
17
  import { SUPPORTED_TRACE_VERSIONS, traceZipLimits, TraceRejected, TraceZipLimits } from './trace-zip-limits';
18
18
 
19
19
  export type TraceEvent = { type: string } & Record<string, unknown>;
20
20
  export interface StackFrame { file: string; line: number; column: number }
21
- export interface LibraryPart { name: string; events: TraceEvent[]; network: TraceEvent[]; stacks: Map<number, StackFrame[]> }
21
+ /**
22
+ * One library part. `version` is its declared format (already accepted by the gate); `stacks` is
23
+ * keyed by call id — v8/v9 wrote a call NUMBER (`11` → `call@11`, Playwright's legacy id), v10 the
24
+ * call id itself, which it shares with the runner step (`ulgt@50`).
25
+ */
26
+ export interface LibraryPart { name: string; version: number; events: TraceEvent[]; network: TraceEvent[]; stacks: Map<string, StackFrame[]> }
22
27
 
23
28
  export interface ParsedTrace {
24
29
  label: string;
@@ -56,19 +61,21 @@ function parseJsonl(zip: ZipArchive, name: string): TraceEvent[] {
56
61
  return out;
57
62
  }
58
63
 
59
- function parseStacks(zip: ZipArchive, name: string): Map<number, StackFrame[]> {
60
- const out = new Map<number, StackFrame[]>();
64
+ function parseStacks(zip: ZipArchive, name: string): Map<string, StackFrame[]> {
65
+ const out = new Map<string, StackFrame[]>();
61
66
  if (!zip.has(name)) return out;
62
67
  let doc: { files?: unknown; stacks?: unknown };
63
68
  try { doc = JSON.parse(zip.readText(name)); } catch { throw new TraceRejected('trace.malformed', `${name} is not JSON`); }
64
69
  const files = Array.isArray(doc.files) ? doc.files.map(String) : [];
65
70
  for (const row of Array.isArray(doc.stacks) ? doc.stacks : []) {
66
- if (!Array.isArray(row) || typeof row[0] !== 'number' || !Array.isArray(row[1])) continue;
71
+ if (!Array.isArray(row) || !Array.isArray(row[1])) continue;
72
+ const id = typeof row[0] === 'number' ? `call@${row[0]}` : row[0];
73
+ if (typeof id !== 'string' || !CALL_ID.test(id)) continue;
67
74
  const frames = (row[1] as unknown[]).filter(Array.isArray).map((f) => {
68
75
  const [fileIdx, line, column] = f as number[];
69
76
  return { file: files[fileIdx] ?? '', line: Number(line) || 0, column: Number(column) || 0 };
70
77
  });
71
- out.set(row[0], frames);
78
+ out.set(id, frames);
72
79
  }
73
80
  return out;
74
81
  }
@@ -81,9 +88,12 @@ export function readTraceBuffer(buf: Buffer, label: string, limits: TraceZipLimi
81
88
  const runner = parseJsonl(zip, 'test.trace');
82
89
  const library: LibraryPart[] = zip.names().filter((n) => /^\d+-trace\.trace$/.test(n)).sort().map((n) => {
83
90
  const base = n.replace(/\.trace$/, '');
91
+ const events = parseJsonl(zip, n);
92
+ const version = headerOf(events)?.version;
84
93
  return {
85
94
  name: n,
86
- events: parseJsonl(zip, n),
95
+ version: typeof version === 'number' ? version : NaN, // a non-number is refused just below
96
+ events,
87
97
  network: zip.has(`${base}.network`) ? parseJsonl(zip, `${base}.network`) : [],
88
98
  stacks: parseStacks(zip, `${base}.stacks`),
89
99
  };
@@ -114,10 +124,21 @@ export function readTraceBuffer(buf: Buffer, label: string, limits: TraceZipLimi
114
124
  };
115
125
  }
116
126
 
117
- /** Read a trace from disk: a regular file, size-checked BEFORE it is read into memory. */
127
+ /**
128
+ * A trace opened once (read-file-once.ts) → parsed: the size was checked on the descriptor BEFORE it
129
+ * was read into memory. `missing` / `outside` are the caller's to report; here they throw.
130
+ */
131
+ export function traceFromRead(read: FileRead, label: string, limits: TraceZipLimits = traceZipLimits()): ParsedTrace {
132
+ switch (read.status) {
133
+ case 'read': return readTraceBuffer(read.bytes, read.real, limits);
134
+ case 'too-large': throw new TraceRejected('zip.too-large', `${read.size} B > ${limits.maxZipBytes}`);
135
+ case 'not-a-file': throw new TraceRejected('zip.malformed', `${label} is not a regular file`);
136
+ case 'missing': throw new TraceRejected('zip.malformed', `${label} does not exist`);
137
+ default: throw new TraceRejected('zip.malformed', `${label} was not read: ${read.why}`);
138
+ }
139
+ }
140
+
141
+ /** Read a trace from a trusted path (a fixture, a test): a regular file, opened once. */
118
142
  export function readTraceFile(file: string, limits: TraceZipLimits = traceZipLimits()): ParsedTrace {
119
- const st = fs.statSync(file);
120
- if (!st.isFile()) throw new TraceRejected('zip.malformed', `${file} is not a regular file`);
121
- if (st.size > limits.maxZipBytes) throw new TraceRejected('zip.too-large', `${st.size} B > ${limits.maxZipBytes}`);
122
- return readTraceBuffer(fs.readFileSync(file), file, limits);
143
+ return traceFromRead(readFileOnce(file, limits.maxZipBytes), file, limits);
123
144
  }