@rpamis/comet 0.4.0-beta.6 → 0.4.0-beta.7

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 (687) hide show
  1. package/README.md +209 -74
  2. package/assets/manifest.json +14 -5
  3. package/assets/skills/comet/SKILL.md +17 -282
  4. package/assets/skills/comet/reference/comet-yaml-fields.md +1 -1
  5. package/assets/skills/comet/reference/context-recovery.md +3 -3
  6. package/assets/skills/comet/reference/intent-frame.md +2 -2
  7. package/assets/skills/comet/reference/scripts.md +2 -2
  8. package/assets/skills/comet/rules/comet-workflow-guard.en.md +32 -0
  9. package/assets/skills/comet/rules/comet-workflow-guard.md +32 -0
  10. package/assets/skills/comet/scripts/comet-entry-runtime.mjs +7834 -0
  11. package/assets/skills/comet/scripts/comet-hook-router.mjs +10307 -0
  12. package/assets/skills/comet/scripts/comet-runtime.mjs +612 -499
  13. package/assets/skills/comet-any/SKILL.md +4 -4
  14. package/assets/skills/comet-any/reference/authoring-subagents.md +1 -1
  15. package/assets/skills/comet-any/reference/bundle-authoring.md +1 -1
  16. package/assets/skills/comet-any/reference/subagents/reference-author.md +1 -1
  17. package/assets/skills/comet-any/reference/subagents/script-author.md +1 -1
  18. package/assets/skills/comet-any/reference/subagents/skill-core-author.md +1 -1
  19. package/assets/skills/comet-any/reference/subagents/skill-reviewer.md +2 -2
  20. package/assets/skills/comet-any/reference/subagents/workflow-entry-author.md +3 -3
  21. package/assets/skills/comet-archive/SKILL.md +1 -2
  22. package/assets/skills/comet-build/SKILL.md +1 -1
  23. package/assets/skills/comet-classic/SKILL.md +293 -0
  24. package/assets/skills/comet-design/SKILL.md +1 -1
  25. package/assets/skills/comet-hotfix/SKILL.md +4 -4
  26. package/assets/skills/comet-native/SKILL.md +257 -0
  27. package/assets/skills/comet-native/reference/artifacts.md +204 -0
  28. package/assets/skills/comet-native/reference/commands.md +118 -0
  29. package/assets/skills/comet-native/reference/recovery.md +143 -0
  30. package/assets/skills/comet-native/scripts/comet-native-hook-guard.mjs +4 -0
  31. package/assets/skills/comet-native/scripts/comet-native-runtime.mjs +28898 -0
  32. package/assets/skills/comet-open/SKILL.md +4 -4
  33. package/assets/skills/comet-tweak/SKILL.md +7 -7
  34. package/assets/skills-zh/comet/SKILL.md +17 -270
  35. package/assets/skills-zh/comet/reference/comet-yaml-fields.md +1 -1
  36. package/assets/skills-zh/comet/reference/context-recovery.md +3 -3
  37. package/assets/skills-zh/comet/reference/intent-frame.md +2 -2
  38. package/assets/skills-zh/comet/reference/scripts.md +2 -2
  39. package/assets/skills-zh/comet-any/SKILL.md +4 -4
  40. package/assets/skills-zh/comet-any/reference/authoring-subagents.md +1 -1
  41. package/assets/skills-zh/comet-any/reference/bundle-authoring.md +1 -1
  42. package/assets/skills-zh/comet-any/reference/subagents/reference-author.md +1 -1
  43. package/assets/skills-zh/comet-any/reference/subagents/script-author.md +1 -1
  44. package/assets/skills-zh/comet-any/reference/subagents/skill-core-author.md +1 -1
  45. package/assets/skills-zh/comet-any/reference/subagents/skill-reviewer.md +2 -2
  46. package/assets/skills-zh/comet-any/reference/subagents/workflow-entry-author.md +3 -3
  47. package/assets/skills-zh/comet-archive/SKILL.md +1 -2
  48. package/assets/skills-zh/comet-build/SKILL.md +1 -1
  49. package/assets/skills-zh/comet-classic/SKILL.md +281 -0
  50. package/assets/skills-zh/comet-design/SKILL.md +1 -1
  51. package/assets/skills-zh/comet-hotfix/SKILL.md +4 -4
  52. package/assets/skills-zh/comet-native/SKILL.md +257 -0
  53. package/assets/skills-zh/comet-native/reference/artifacts.md +206 -0
  54. package/assets/skills-zh/comet-native/reference/commands.md +118 -0
  55. package/assets/skills-zh/comet-native/reference/recovery.md +143 -0
  56. package/assets/skills-zh/comet-open/SKILL.md +4 -4
  57. package/assets/skills-zh/comet-tweak/SKILL.md +7 -7
  58. package/dist/app/cli/index.js +59 -13
  59. package/dist/app/cli/index.js.map +1 -1
  60. package/dist/app/commands/command-result.d.ts +6 -0
  61. package/dist/app/commands/command-result.d.ts.map +1 -0
  62. package/dist/app/commands/command-result.js +4 -0
  63. package/dist/app/commands/command-result.js.map +1 -0
  64. package/dist/app/commands/doctor.d.ts +1 -0
  65. package/dist/app/commands/doctor.d.ts.map +1 -1
  66. package/dist/app/commands/doctor.js +201 -26
  67. package/dist/app/commands/doctor.js.map +1 -1
  68. package/dist/app/commands/eval.d.ts +3 -1
  69. package/dist/app/commands/eval.d.ts.map +1 -1
  70. package/dist/app/commands/eval.js +17 -7
  71. package/dist/app/commands/eval.js.map +1 -1
  72. package/dist/app/commands/i18n.d.ts +1 -1
  73. package/dist/app/commands/i18n.d.ts.map +1 -1
  74. package/dist/app/commands/i18n.js +6 -2
  75. package/dist/app/commands/i18n.js.map +1 -1
  76. package/dist/app/commands/init.d.ts +10 -2
  77. package/dist/app/commands/init.d.ts.map +1 -1
  78. package/dist/app/commands/init.js +331 -50
  79. package/dist/app/commands/init.js.map +1 -1
  80. package/dist/app/commands/native.d.ts +2 -0
  81. package/dist/app/commands/native.d.ts.map +1 -0
  82. package/dist/app/commands/native.js +10 -0
  83. package/dist/app/commands/native.js.map +1 -0
  84. package/dist/app/commands/project-scope-selection.d.ts.map +1 -1
  85. package/dist/app/commands/project-scope-selection.js +2 -0
  86. package/dist/app/commands/project-scope-selection.js.map +1 -1
  87. package/dist/app/commands/resume-probe.d.ts.map +1 -1
  88. package/dist/app/commands/resume-probe.js +8 -2
  89. package/dist/app/commands/resume-probe.js.map +1 -1
  90. package/dist/app/commands/status.d.ts +0 -32
  91. package/dist/app/commands/status.d.ts.map +1 -1
  92. package/dist/app/commands/status.js +44 -177
  93. package/dist/app/commands/status.js.map +1 -1
  94. package/dist/app/commands/update.d.ts +20 -4
  95. package/dist/app/commands/update.d.ts.map +1 -1
  96. package/dist/app/commands/update.js +698 -90
  97. package/dist/app/commands/update.js.map +1 -1
  98. package/dist/app/commands/workflow.d.ts +6 -0
  99. package/dist/app/commands/workflow.d.ts.map +1 -0
  100. package/dist/app/commands/workflow.js +11 -0
  101. package/dist/app/commands/workflow.js.map +1 -0
  102. package/dist/config/repository-layout.json +21 -1
  103. package/dist/domains/comet-classic/classic-archive.d.ts.map +1 -1
  104. package/dist/domains/comet-classic/classic-archive.js +3 -0
  105. package/dist/domains/comet-classic/classic-archive.js.map +1 -1
  106. package/dist/domains/comet-classic/classic-current-change.d.ts +3 -5
  107. package/dist/domains/comet-classic/classic-current-change.d.ts.map +1 -1
  108. package/dist/domains/comet-classic/classic-current-change.js +31 -57
  109. package/dist/domains/comet-classic/classic-current-change.js.map +1 -1
  110. package/dist/domains/comet-classic/classic-hook-guard.d.ts +9 -0
  111. package/dist/domains/comet-classic/classic-hook-guard.d.ts.map +1 -1
  112. package/dist/domains/comet-classic/classic-hook-guard.js +84 -11
  113. package/dist/domains/comet-classic/classic-hook-guard.js.map +1 -1
  114. package/dist/domains/comet-classic/classic-project-config.d.ts.map +1 -1
  115. package/dist/domains/comet-classic/classic-project-config.js +12 -8
  116. package/dist/domains/comet-classic/classic-project-config.js.map +1 -1
  117. package/dist/domains/comet-classic/classic-runtime-run.d.ts.map +1 -1
  118. package/dist/domains/comet-classic/classic-runtime-run.js +9 -1
  119. package/dist/domains/comet-classic/classic-runtime-run.js.map +1 -1
  120. package/dist/domains/comet-entry/current-selection-repair.d.ts +17 -0
  121. package/dist/domains/comet-entry/current-selection-repair.d.ts.map +1 -0
  122. package/dist/domains/comet-entry/current-selection-repair.js +19 -0
  123. package/dist/domains/comet-entry/current-selection-repair.js.map +1 -0
  124. package/dist/domains/comet-entry/current-selection.d.ts +28 -0
  125. package/dist/domains/comet-entry/current-selection.d.ts.map +1 -0
  126. package/dist/domains/comet-entry/current-selection.js +116 -0
  127. package/dist/domains/comet-entry/current-selection.js.map +1 -0
  128. package/dist/domains/comet-entry/entry-runtime-entry.d.ts +2 -0
  129. package/dist/domains/comet-entry/entry-runtime-entry.d.ts.map +1 -0
  130. package/dist/domains/comet-entry/entry-runtime-entry.js +3 -0
  131. package/dist/domains/comet-entry/entry-runtime-entry.js.map +1 -0
  132. package/dist/domains/comet-entry/entry-runtime.d.ts +7 -0
  133. package/dist/domains/comet-entry/entry-runtime.d.ts.map +1 -0
  134. package/dist/domains/comet-entry/entry-runtime.js +55 -0
  135. package/dist/domains/comet-entry/entry-runtime.js.map +1 -0
  136. package/dist/domains/comet-entry/hook-adapter.d.ts +6 -0
  137. package/dist/domains/comet-entry/hook-adapter.d.ts.map +1 -0
  138. package/dist/domains/comet-entry/hook-adapter.js +202 -0
  139. package/dist/domains/comet-entry/hook-adapter.js.map +1 -0
  140. package/dist/domains/comet-entry/hook-router-entry.d.ts +8 -0
  141. package/dist/domains/comet-entry/hook-router-entry.d.ts.map +1 -0
  142. package/dist/domains/comet-entry/hook-router-entry.js +92 -0
  143. package/dist/domains/comet-entry/hook-router-entry.js.map +1 -0
  144. package/dist/domains/comet-entry/hook-router.d.ts +32 -0
  145. package/dist/domains/comet-entry/hook-router.d.ts.map +1 -0
  146. package/dist/domains/comet-entry/hook-router.js +133 -0
  147. package/dist/domains/comet-entry/hook-router.js.map +1 -0
  148. package/dist/domains/comet-entry/hook-types.d.ts +20 -0
  149. package/dist/domains/comet-entry/hook-types.d.ts.map +1 -0
  150. package/dist/domains/comet-entry/hook-types.js +2 -0
  151. package/dist/domains/comet-entry/hook-types.js.map +1 -0
  152. package/dist/domains/comet-entry/init-workflow.d.ts +16 -0
  153. package/dist/domains/comet-entry/init-workflow.d.ts.map +1 -0
  154. package/dist/domains/comet-entry/init-workflow.js +103 -0
  155. package/dist/domains/comet-entry/init-workflow.js.map +1 -0
  156. package/dist/domains/comet-entry/project-status.d.ts +3 -0
  157. package/dist/domains/comet-entry/project-status.d.ts.map +1 -0
  158. package/dist/domains/comet-entry/project-status.js +222 -0
  159. package/dist/domains/comet-entry/project-status.js.map +1 -0
  160. package/dist/domains/comet-entry/resolve-entry.d.ts +3 -0
  161. package/dist/domains/comet-entry/resolve-entry.d.ts.map +1 -0
  162. package/dist/domains/comet-entry/resolve-entry.js +22 -0
  163. package/dist/domains/comet-entry/resolve-entry.js.map +1 -0
  164. package/dist/domains/comet-entry/resume-probe.d.ts +34 -0
  165. package/dist/domains/comet-entry/resume-probe.d.ts.map +1 -0
  166. package/dist/domains/comet-entry/resume-probe.js +458 -0
  167. package/dist/domains/comet-entry/resume-probe.js.map +1 -0
  168. package/dist/domains/comet-entry/types.d.ts +59 -0
  169. package/dist/domains/comet-entry/types.d.ts.map +1 -0
  170. package/dist/domains/comet-entry/types.js +2 -0
  171. package/dist/domains/comet-entry/types.js.map +1 -0
  172. package/dist/domains/comet-entry/workflow-resolution.d.ts +8 -0
  173. package/dist/domains/comet-entry/workflow-resolution.d.ts.map +1 -0
  174. package/dist/domains/comet-entry/workflow-resolution.js +16 -0
  175. package/dist/domains/comet-entry/workflow-resolution.js.map +1 -0
  176. package/dist/domains/comet-native/index.d.ts +35 -0
  177. package/dist/domains/comet-native/index.d.ts.map +1 -0
  178. package/dist/domains/comet-native/index.js +35 -0
  179. package/dist/domains/comet-native/index.js.map +1 -0
  180. package/dist/domains/comet-native/native-acceptance.d.ts +42 -0
  181. package/dist/domains/comet-native/native-acceptance.d.ts.map +1 -0
  182. package/dist/domains/comet-native/native-acceptance.js +503 -0
  183. package/dist/domains/comet-native/native-acceptance.js.map +1 -0
  184. package/dist/domains/comet-native/native-archive-content.d.ts +26 -0
  185. package/dist/domains/comet-native/native-archive-content.d.ts.map +1 -0
  186. package/dist/domains/comet-native/native-archive-content.js +159 -0
  187. package/dist/domains/comet-native/native-archive-content.js.map +1 -0
  188. package/dist/domains/comet-native/native-archive-inspection.d.ts +9 -0
  189. package/dist/domains/comet-native/native-archive-inspection.d.ts.map +1 -0
  190. package/dist/domains/comet-native/native-archive-inspection.js +94 -0
  191. package/dist/domains/comet-native/native-archive-inspection.js.map +1 -0
  192. package/dist/domains/comet-native/native-archive-preflight.d.ts +52 -0
  193. package/dist/domains/comet-native/native-archive-preflight.d.ts.map +1 -0
  194. package/dist/domains/comet-native/native-archive-preflight.js +214 -0
  195. package/dist/domains/comet-native/native-archive-preflight.js.map +1 -0
  196. package/dist/domains/comet-native/native-archive-transaction.d.ts +19 -0
  197. package/dist/domains/comet-native/native-archive-transaction.d.ts.map +1 -0
  198. package/dist/domains/comet-native/native-archive-transaction.js +1181 -0
  199. package/dist/domains/comet-native/native-archive-transaction.js.map +1 -0
  200. package/dist/domains/comet-native/native-archive.d.ts +36 -0
  201. package/dist/domains/comet-native/native-archive.d.ts.map +1 -0
  202. package/dist/domains/comet-native/native-archive.js +403 -0
  203. package/dist/domains/comet-native/native-archive.js.map +1 -0
  204. package/dist/domains/comet-native/native-artifacts.d.ts +10 -0
  205. package/dist/domains/comet-native/native-artifacts.d.ts.map +1 -0
  206. package/dist/domains/comet-native/native-artifacts.js +204 -0
  207. package/dist/domains/comet-native/native-artifacts.js.map +1 -0
  208. package/dist/domains/comet-native/native-atomic-file.d.ts +10 -0
  209. package/dist/domains/comet-native/native-atomic-file.d.ts.map +1 -0
  210. package/dist/domains/comet-native/native-atomic-file.js +175 -0
  211. package/dist/domains/comet-native/native-atomic-file.js.map +1 -0
  212. package/dist/domains/comet-native/native-bounded-file.d.ts +19 -0
  213. package/dist/domains/comet-native/native-bounded-file.d.ts.map +1 -0
  214. package/dist/domains/comet-native/native-bounded-file.js +180 -0
  215. package/dist/domains/comet-native/native-bounded-file.js.map +1 -0
  216. package/dist/domains/comet-native/native-build-evidence.d.ts +42 -0
  217. package/dist/domains/comet-native/native-build-evidence.d.ts.map +1 -0
  218. package/dist/domains/comet-native/native-build-evidence.js +257 -0
  219. package/dist/domains/comet-native/native-build-evidence.js.map +1 -0
  220. package/dist/domains/comet-native/native-canonical-hash.d.ts +10 -0
  221. package/dist/domains/comet-native/native-canonical-hash.d.ts.map +1 -0
  222. package/dist/domains/comet-native/native-canonical-hash.js +100 -0
  223. package/dist/domains/comet-native/native-canonical-hash.js.map +1 -0
  224. package/dist/domains/comet-native/native-change-recovery.d.ts +11 -0
  225. package/dist/domains/comet-native/native-change-recovery.d.ts.map +1 -0
  226. package/dist/domains/comet-native/native-change-recovery.js +15 -0
  227. package/dist/domains/comet-native/native-change-recovery.js.map +1 -0
  228. package/dist/domains/comet-native/native-change.d.ts +61 -0
  229. package/dist/domains/comet-native/native-change.d.ts.map +1 -0
  230. package/dist/domains/comet-native/native-change.js +696 -0
  231. package/dist/domains/comet-native/native-change.js.map +1 -0
  232. package/dist/domains/comet-native/native-check-receipt-model.d.ts +63 -0
  233. package/dist/domains/comet-native/native-check-receipt-model.d.ts.map +1 -0
  234. package/dist/domains/comet-native/native-check-receipt-model.js +347 -0
  235. package/dist/domains/comet-native/native-check-receipt-model.js.map +1 -0
  236. package/dist/domains/comet-native/native-check-receipt-storage.d.ts +10 -0
  237. package/dist/domains/comet-native/native-check-receipt-storage.d.ts.map +1 -0
  238. package/dist/domains/comet-native/native-check-receipt-storage.js +196 -0
  239. package/dist/domains/comet-native/native-check-receipt-storage.js.map +1 -0
  240. package/dist/domains/comet-native/native-check-receipt.d.ts +19 -0
  241. package/dist/domains/comet-native/native-check-receipt.d.ts.map +1 -0
  242. package/dist/domains/comet-native/native-check-receipt.js +446 -0
  243. package/dist/domains/comet-native/native-check-receipt.js.map +1 -0
  244. package/dist/domains/comet-native/native-check.d.ts +18 -0
  245. package/dist/domains/comet-native/native-check.d.ts.map +1 -0
  246. package/dist/domains/comet-native/native-check.js +29 -0
  247. package/dist/domains/comet-native/native-check.js.map +1 -0
  248. package/dist/domains/comet-native/native-checkpoint-journal.d.ts +13 -0
  249. package/dist/domains/comet-native/native-checkpoint-journal.d.ts.map +1 -0
  250. package/dist/domains/comet-native/native-checkpoint-journal.js +53 -0
  251. package/dist/domains/comet-native/native-checkpoint-journal.js.map +1 -0
  252. package/dist/domains/comet-native/native-checkpoint-storage.d.ts +43 -0
  253. package/dist/domains/comet-native/native-checkpoint-storage.d.ts.map +1 -0
  254. package/dist/domains/comet-native/native-checkpoint-storage.js +487 -0
  255. package/dist/domains/comet-native/native-checkpoint-storage.js.map +1 -0
  256. package/dist/domains/comet-native/native-cli-entry.d.ts +2 -0
  257. package/dist/domains/comet-native/native-cli-entry.d.ts.map +1 -0
  258. package/dist/domains/comet-native/native-cli-entry.js +18 -0
  259. package/dist/domains/comet-native/native-cli-entry.js.map +1 -0
  260. package/dist/domains/comet-native/native-cli.d.ts +7 -0
  261. package/dist/domains/comet-native/native-cli.d.ts.map +1 -0
  262. package/dist/domains/comet-native/native-cli.js +642 -0
  263. package/dist/domains/comet-native/native-cli.js.map +1 -0
  264. package/dist/domains/comet-native/native-config.d.ts +15 -0
  265. package/dist/domains/comet-native/native-config.d.ts.map +1 -0
  266. package/dist/domains/comet-native/native-config.js +281 -0
  267. package/dist/domains/comet-native/native-config.js.map +1 -0
  268. package/dist/domains/comet-native/native-conflict-inspection.d.ts +17 -0
  269. package/dist/domains/comet-native/native-conflict-inspection.d.ts.map +1 -0
  270. package/dist/domains/comet-native/native-conflict-inspection.js +90 -0
  271. package/dist/domains/comet-native/native-conflict-inspection.js.map +1 -0
  272. package/dist/domains/comet-native/native-conflict-radar.d.ts +75 -0
  273. package/dist/domains/comet-native/native-conflict-radar.d.ts.map +1 -0
  274. package/dist/domains/comet-native/native-conflict-radar.js +326 -0
  275. package/dist/domains/comet-native/native-conflict-radar.js.map +1 -0
  276. package/dist/domains/comet-native/native-continuation.d.ts +9 -0
  277. package/dist/domains/comet-native/native-continuation.d.ts.map +1 -0
  278. package/dist/domains/comet-native/native-continuation.js +146 -0
  279. package/dist/domains/comet-native/native-continuation.js.map +1 -0
  280. package/dist/domains/comet-native/native-contract-files.d.ts +24 -0
  281. package/dist/domains/comet-native/native-contract-files.d.ts.map +1 -0
  282. package/dist/domains/comet-native/native-contract-files.js +66 -0
  283. package/dist/domains/comet-native/native-contract-files.js.map +1 -0
  284. package/dist/domains/comet-native/native-contract.d.ts +41 -0
  285. package/dist/domains/comet-native/native-contract.d.ts.map +1 -0
  286. package/dist/domains/comet-native/native-contract.js +141 -0
  287. package/dist/domains/comet-native/native-contract.js.map +1 -0
  288. package/dist/domains/comet-native/native-diagnostics.d.ts +18 -0
  289. package/dist/domains/comet-native/native-diagnostics.d.ts.map +1 -0
  290. package/dist/domains/comet-native/native-diagnostics.js +522 -0
  291. package/dist/domains/comet-native/native-diagnostics.js.map +1 -0
  292. package/dist/domains/comet-native/native-doctor.d.ts +12 -0
  293. package/dist/domains/comet-native/native-doctor.d.ts.map +1 -0
  294. package/dist/domains/comet-native/native-doctor.js +768 -0
  295. package/dist/domains/comet-native/native-doctor.js.map +1 -0
  296. package/dist/domains/comet-native/native-evidence-retention.d.ts +37 -0
  297. package/dist/domains/comet-native/native-evidence-retention.d.ts.map +1 -0
  298. package/dist/domains/comet-native/native-evidence-retention.js +885 -0
  299. package/dist/domains/comet-native/native-evidence-retention.js.map +1 -0
  300. package/dist/domains/comet-native/native-evidence-storage.d.ts +41 -0
  301. package/dist/domains/comet-native/native-evidence-storage.d.ts.map +1 -0
  302. package/dist/domains/comet-native/native-evidence-storage.js +351 -0
  303. package/dist/domains/comet-native/native-evidence-storage.js.map +1 -0
  304. package/dist/domains/comet-native/native-findings.d.ts +8 -0
  305. package/dist/domains/comet-native/native-findings.d.ts.map +1 -0
  306. package/dist/domains/comet-native/native-findings.js +238 -0
  307. package/dist/domains/comet-native/native-findings.js.map +1 -0
  308. package/dist/domains/comet-native/native-guards.d.ts +7 -0
  309. package/dist/domains/comet-native/native-guards.d.ts.map +1 -0
  310. package/dist/domains/comet-native/native-guards.js +126 -0
  311. package/dist/domains/comet-native/native-guards.js.map +1 -0
  312. package/dist/domains/comet-native/native-hash.d.ts +3 -0
  313. package/dist/domains/comet-native/native-hash.d.ts.map +1 -0
  314. package/dist/domains/comet-native/native-hash.js +12 -0
  315. package/dist/domains/comet-native/native-hash.js.map +1 -0
  316. package/dist/domains/comet-native/native-hook-guard.d.ts +17 -0
  317. package/dist/domains/comet-native/native-hook-guard.d.ts.map +1 -0
  318. package/dist/domains/comet-native/native-hook-guard.js +154 -0
  319. package/dist/domains/comet-native/native-hook-guard.js.map +1 -0
  320. package/dist/domains/comet-native/native-lock.d.ts +47 -0
  321. package/dist/domains/comet-native/native-lock.d.ts.map +1 -0
  322. package/dist/domains/comet-native/native-lock.js +395 -0
  323. package/dist/domains/comet-native/native-lock.js.map +1 -0
  324. package/dist/domains/comet-native/native-mutation-lock.d.ts +5 -0
  325. package/dist/domains/comet-native/native-mutation-lock.d.ts.map +1 -0
  326. package/dist/domains/comet-native/native-mutation-lock.js +66 -0
  327. package/dist/domains/comet-native/native-mutation-lock.js.map +1 -0
  328. package/dist/domains/comet-native/native-paths.d.ts +10 -0
  329. package/dist/domains/comet-native/native-paths.d.ts.map +1 -0
  330. package/dist/domains/comet-native/native-paths.js +171 -0
  331. package/dist/domains/comet-native/native-paths.js.map +1 -0
  332. package/dist/domains/comet-native/native-progress-checkpoint.d.ts +13 -0
  333. package/dist/domains/comet-native/native-progress-checkpoint.d.ts.map +1 -0
  334. package/dist/domains/comet-native/native-progress-checkpoint.js +104 -0
  335. package/dist/domains/comet-native/native-progress-checkpoint.js.map +1 -0
  336. package/dist/domains/comet-native/native-protected-file.d.ts +113 -0
  337. package/dist/domains/comet-native/native-protected-file.d.ts.map +1 -0
  338. package/dist/domains/comet-native/native-protected-file.js +509 -0
  339. package/dist/domains/comet-native/native-protected-file.js.map +1 -0
  340. package/dist/domains/comet-native/native-redaction.d.ts +2 -0
  341. package/dist/domains/comet-native/native-redaction.d.ts.map +1 -0
  342. package/dist/domains/comet-native/native-redaction.js +18 -0
  343. package/dist/domains/comet-native/native-redaction.js.map +1 -0
  344. package/dist/domains/comet-native/native-repair-integration.d.ts +42 -0
  345. package/dist/domains/comet-native/native-repair-integration.d.ts.map +1 -0
  346. package/dist/domains/comet-native/native-repair-integration.js +184 -0
  347. package/dist/domains/comet-native/native-repair-integration.js.map +1 -0
  348. package/dist/domains/comet-native/native-repair-runtime.d.ts +73 -0
  349. package/dist/domains/comet-native/native-repair-runtime.d.ts.map +1 -0
  350. package/dist/domains/comet-native/native-repair-runtime.js +509 -0
  351. package/dist/domains/comet-native/native-repair-runtime.js.map +1 -0
  352. package/dist/domains/comet-native/native-repair-stagnation.d.ts +85 -0
  353. package/dist/domains/comet-native/native-repair-stagnation.d.ts.map +1 -0
  354. package/dist/domains/comet-native/native-repair-stagnation.js +256 -0
  355. package/dist/domains/comet-native/native-repair-stagnation.js.map +1 -0
  356. package/dist/domains/comet-native/native-resume-view.d.ts +14 -0
  357. package/dist/domains/comet-native/native-resume-view.d.ts.map +1 -0
  358. package/dist/domains/comet-native/native-resume-view.js +105 -0
  359. package/dist/domains/comet-native/native-resume-view.js.map +1 -0
  360. package/dist/domains/comet-native/native-revision.d.ts +20 -0
  361. package/dist/domains/comet-native/native-revision.d.ts.map +1 -0
  362. package/dist/domains/comet-native/native-revision.js +32 -0
  363. package/dist/domains/comet-native/native-revision.js.map +1 -0
  364. package/dist/domains/comet-native/native-root-move.d.ts +20 -0
  365. package/dist/domains/comet-native/native-root-move.d.ts.map +1 -0
  366. package/dist/domains/comet-native/native-root-move.js +812 -0
  367. package/dist/domains/comet-native/native-root-move.js.map +1 -0
  368. package/dist/domains/comet-native/native-run-consistency.d.ts +3 -0
  369. package/dist/domains/comet-native/native-run-consistency.d.ts.map +1 -0
  370. package/dist/domains/comet-native/native-run-consistency.js +136 -0
  371. package/dist/domains/comet-native/native-run-consistency.js.map +1 -0
  372. package/dist/domains/comet-native/native-run-store.d.ts +41 -0
  373. package/dist/domains/comet-native/native-run-store.d.ts.map +1 -0
  374. package/dist/domains/comet-native/native-run-store.js +451 -0
  375. package/dist/domains/comet-native/native-run-store.js.map +1 -0
  376. package/dist/domains/comet-native/native-runtime-package.d.ts +16 -0
  377. package/dist/domains/comet-native/native-runtime-package.d.ts.map +1 -0
  378. package/dist/domains/comet-native/native-runtime-package.js +72 -0
  379. package/dist/domains/comet-native/native-runtime-package.js.map +1 -0
  380. package/dist/domains/comet-native/native-schema-migration.d.ts +11 -0
  381. package/dist/domains/comet-native/native-schema-migration.d.ts.map +1 -0
  382. package/dist/domains/comet-native/native-schema-migration.js +899 -0
  383. package/dist/domains/comet-native/native-schema-migration.js.map +1 -0
  384. package/dist/domains/comet-native/native-selection.d.ts +12 -0
  385. package/dist/domains/comet-native/native-selection.d.ts.map +1 -0
  386. package/dist/domains/comet-native/native-selection.js +52 -0
  387. package/dist/domains/comet-native/native-selection.js.map +1 -0
  388. package/dist/domains/comet-native/native-sensitive-paths.d.ts +7 -0
  389. package/dist/domains/comet-native/native-sensitive-paths.d.ts.map +1 -0
  390. package/dist/domains/comet-native/native-sensitive-paths.js +69 -0
  391. package/dist/domains/comet-native/native-sensitive-paths.js.map +1 -0
  392. package/dist/domains/comet-native/native-snapshot.d.ts +74 -0
  393. package/dist/domains/comet-native/native-snapshot.d.ts.map +1 -0
  394. package/dist/domains/comet-native/native-snapshot.js +2308 -0
  395. package/dist/domains/comet-native/native-snapshot.js.map +1 -0
  396. package/dist/domains/comet-native/native-specs.d.ts +12 -0
  397. package/dist/domains/comet-native/native-specs.d.ts.map +1 -0
  398. package/dist/domains/comet-native/native-specs.js +270 -0
  399. package/dist/domains/comet-native/native-specs.js.map +1 -0
  400. package/dist/domains/comet-native/native-trajectory-limits.d.ts +7 -0
  401. package/dist/domains/comet-native/native-trajectory-limits.d.ts.map +1 -0
  402. package/dist/domains/comet-native/native-trajectory-limits.js +13 -0
  403. package/dist/domains/comet-native/native-trajectory-limits.js.map +1 -0
  404. package/dist/domains/comet-native/native-trajectory-recovery.d.ts +37 -0
  405. package/dist/domains/comet-native/native-trajectory-recovery.d.ts.map +1 -0
  406. package/dist/domains/comet-native/native-trajectory-recovery.js +133 -0
  407. package/dist/domains/comet-native/native-trajectory-recovery.js.map +1 -0
  408. package/dist/domains/comet-native/native-trajectory.d.ts +16 -0
  409. package/dist/domains/comet-native/native-trajectory.d.ts.map +1 -0
  410. package/dist/domains/comet-native/native-trajectory.js +27 -0
  411. package/dist/domains/comet-native/native-trajectory.js.map +1 -0
  412. package/dist/domains/comet-native/native-transaction.d.ts +45 -0
  413. package/dist/domains/comet-native/native-transaction.d.ts.map +1 -0
  414. package/dist/domains/comet-native/native-transaction.js +846 -0
  415. package/dist/domains/comet-native/native-transaction.js.map +1 -0
  416. package/dist/domains/comet-native/native-transition-evidence.d.ts +4 -0
  417. package/dist/domains/comet-native/native-transition-evidence.d.ts.map +1 -0
  418. package/dist/domains/comet-native/native-transition-evidence.js +20 -0
  419. package/dist/domains/comet-native/native-transition-evidence.js.map +1 -0
  420. package/dist/domains/comet-native/native-transition-journal.d.ts +30 -0
  421. package/dist/domains/comet-native/native-transition-journal.d.ts.map +1 -0
  422. package/dist/domains/comet-native/native-transition-journal.js +819 -0
  423. package/dist/domains/comet-native/native-transition-journal.js.map +1 -0
  424. package/dist/domains/comet-native/native-transitions.d.ts +13 -0
  425. package/dist/domains/comet-native/native-transitions.d.ts.map +1 -0
  426. package/dist/domains/comet-native/native-transitions.js +632 -0
  427. package/dist/domains/comet-native/native-transitions.js.map +1 -0
  428. package/dist/domains/comet-native/native-types.d.ts +547 -0
  429. package/dist/domains/comet-native/native-types.d.ts.map +1 -0
  430. package/dist/domains/comet-native/native-types.js +8 -0
  431. package/dist/domains/comet-native/native-types.js.map +1 -0
  432. package/dist/domains/comet-native/native-verification-evidence.d.ts +86 -0
  433. package/dist/domains/comet-native/native-verification-evidence.d.ts.map +1 -0
  434. package/dist/domains/comet-native/native-verification-evidence.js +480 -0
  435. package/dist/domains/comet-native/native-verification-evidence.js.map +1 -0
  436. package/dist/domains/comet-native/native-verification-runtime.d.ts +58 -0
  437. package/dist/domains/comet-native/native-verification-runtime.d.ts.map +1 -0
  438. package/dist/domains/comet-native/native-verification-runtime.js +321 -0
  439. package/dist/domains/comet-native/native-verification-runtime.js.map +1 -0
  440. package/dist/domains/comet-native/native-verification-scope.d.ts +117 -0
  441. package/dist/domains/comet-native/native-verification-scope.d.ts.map +1 -0
  442. package/dist/domains/comet-native/native-verification-scope.js +1040 -0
  443. package/dist/domains/comet-native/native-verification-scope.js.map +1 -0
  444. package/dist/domains/comet-native/native-workspace.d.ts +46 -0
  445. package/dist/domains/comet-native/native-workspace.d.ts.map +1 -0
  446. package/dist/domains/comet-native/native-workspace.js +276 -0
  447. package/dist/domains/comet-native/native-workspace.js.map +1 -0
  448. package/dist/domains/dashboard/collector.d.ts.map +1 -1
  449. package/dist/domains/dashboard/collector.js +9 -1
  450. package/dist/domains/dashboard/collector.js.map +1 -1
  451. package/dist/domains/dashboard/native-adapter.d.ts +157 -0
  452. package/dist/domains/dashboard/native-adapter.d.ts.map +1 -0
  453. package/dist/domains/dashboard/native-adapter.js +302 -0
  454. package/dist/domains/dashboard/native-adapter.js.map +1 -0
  455. package/dist/domains/dashboard/native-collector.d.ts +6 -0
  456. package/dist/domains/dashboard/native-collector.d.ts.map +1 -0
  457. package/dist/domains/dashboard/native-collector.js +285 -0
  458. package/dist/domains/dashboard/native-collector.js.map +1 -0
  459. package/dist/domains/dashboard/types.d.ts +5 -0
  460. package/dist/domains/dashboard/types.d.ts.map +1 -1
  461. package/dist/domains/dashboard/web/assets/{arc-TGVUG2WN.js → arc-CxdiLJ50.js} +1 -1
  462. package/dist/domains/dashboard/web/assets/{architectureDiagram-3BPJPVTR-ASHmiHoP.js → architectureDiagram-3BPJPVTR-D5YCQu4e.js} +1 -1
  463. package/dist/domains/dashboard/web/assets/{blockDiagram-GPEHLZMM-CwASxBKe.js → blockDiagram-GPEHLZMM-8NfdCOQ8.js} +1 -1
  464. package/dist/domains/dashboard/web/assets/{c4Diagram-AAUBKEIU-BU8z_WNk.js → c4Diagram-AAUBKEIU-B3cFnNT7.js} +1 -1
  465. package/dist/domains/dashboard/web/assets/channel-B6AFly29.js +1 -0
  466. package/dist/domains/dashboard/web/assets/{chunk-2J33WTMH-CBGlUx4_.js → chunk-2J33WTMH-CBNfW-Dr.js} +1 -1
  467. package/dist/domains/dashboard/web/assets/{chunk-4BX2VUAB-CS-iGcFz.js → chunk-4BX2VUAB-vWyemRBr.js} +1 -1
  468. package/dist/domains/dashboard/web/assets/{chunk-55IACEB6-BUtCVJat.js → chunk-55IACEB6-H6haOo2C.js} +1 -1
  469. package/dist/domains/dashboard/web/assets/{chunk-727SXJPM-C94tj_pf.js → chunk-727SXJPM-D02UjGND.js} +1 -1
  470. package/dist/domains/dashboard/web/assets/{chunk-AQP2D5EJ-BFOKcpdG.js → chunk-AQP2D5EJ-wKsbOw_T.js} +1 -1
  471. package/dist/domains/dashboard/web/assets/{chunk-FMBD7UC4-Dmx8B-st.js → chunk-FMBD7UC4-eYFEZimP.js} +1 -1
  472. package/dist/domains/dashboard/web/assets/{chunk-ND2GUHAM-DYV00CNn.js → chunk-ND2GUHAM-fMaIDDI2.js} +1 -1
  473. package/dist/domains/dashboard/web/assets/{chunk-QZHKN3VN-DMJJyQnw.js → chunk-QZHKN3VN-C9rhF6Hm.js} +1 -1
  474. package/dist/domains/dashboard/web/assets/classDiagram-4FO5ZUOK-BcnrxOTt.js +1 -0
  475. package/dist/domains/dashboard/web/assets/classDiagram-v2-Q7XG4LA2-BcnrxOTt.js +1 -0
  476. package/dist/domains/dashboard/web/assets/{cose-bilkent-S5V4N54A-P4nE2rea.js → cose-bilkent-S5V4N54A-CbaShPub.js} +1 -1
  477. package/dist/domains/dashboard/web/assets/{cynefin-VYW2F7L2-C-R1HXDv.js → cynefin-VYW2F7L2-DN4emAQv.js} +1 -1
  478. package/dist/domains/dashboard/web/assets/{dagre-BM42HDAG-BH8d-C9z.js → dagre-BM42HDAG-BpSzbk_V.js} +1 -1
  479. package/dist/domains/dashboard/web/assets/demo-BuVau32v.js +25 -0
  480. package/dist/domains/dashboard/web/assets/{diagram-2AECGRRQ-DpXTgQIH.js → diagram-2AECGRRQ-CfXwGGpQ.js} +1 -1
  481. package/dist/domains/dashboard/web/assets/{diagram-5GNKFQAL-DSiFJzSw.js → diagram-5GNKFQAL-DMp0Zmot.js} +1 -1
  482. package/dist/domains/dashboard/web/assets/{diagram-KO2AKTUF-DULMkben.js → diagram-KO2AKTUF-C7ZvofsM.js} +1 -1
  483. package/dist/domains/dashboard/web/assets/{diagram-LMA3HP47-Pi8m_NO9.js → diagram-LMA3HP47-BjmxFnap.js} +1 -1
  484. package/dist/domains/dashboard/web/assets/{diagram-OG6HWLK6-BSe6bCWm.js → diagram-OG6HWLK6-DFBG1Iqy.js} +1 -1
  485. package/dist/domains/dashboard/web/assets/{erDiagram-TEJ5UH35-zb-FEcbq.js → erDiagram-TEJ5UH35-MpNT7dzt.js} +1 -1
  486. package/dist/domains/dashboard/web/assets/{flowDiagram-I6XJVG4X-Dr-Gk0z2.js → flowDiagram-I6XJVG4X-_1wAR6c1.js} +1 -1
  487. package/dist/domains/dashboard/web/assets/{ganttDiagram-6RSMTGT7-qs6Zb2A6.js → ganttDiagram-6RSMTGT7-DJKjDeWl.js} +1 -1
  488. package/dist/domains/dashboard/web/assets/{gitGraphDiagram-PVQCEYII-BQ314orT.js → gitGraphDiagram-PVQCEYII-BL7yUEn-.js} +1 -1
  489. package/dist/domains/dashboard/web/assets/index-CjE9mHGb.js +24 -0
  490. package/dist/domains/dashboard/web/assets/index-D-AGxvI4.css +1 -0
  491. package/dist/domains/dashboard/web/assets/{infoDiagram-5YYISTIA-O0bQp98A.js → infoDiagram-5YYISTIA-Bt5fqrOd.js} +1 -1
  492. package/dist/domains/dashboard/web/assets/{ishikawaDiagram-YF4QCWOH-BPC7wbKh.js → ishikawaDiagram-YF4QCWOH-COCMcjBM.js} +1 -1
  493. package/dist/domains/dashboard/web/assets/{journeyDiagram-JHISSGLW-ohViOd4b.js → journeyDiagram-JHISSGLW-DGcpcWny.js} +1 -1
  494. package/dist/domains/dashboard/web/assets/{kanban-definition-UN3LZRKU-CHleNdbG.js → kanban-definition-UN3LZRKU-QSBNsWMP.js} +1 -1
  495. package/dist/domains/dashboard/web/assets/{linear-CyqiW--Q.js → linear-9uyeeBZf.js} +1 -1
  496. package/dist/domains/dashboard/web/assets/{mermaid.core-qS9XvtaK.js → mermaid.core-CkNdB471.js} +4 -4
  497. package/dist/domains/dashboard/web/assets/{mindmap-definition-RKZ34NQL-oaGkqO9l.js → mindmap-definition-RKZ34NQL-CiNxI0gP.js} +1 -1
  498. package/dist/domains/dashboard/web/assets/{pieDiagram-4H26LBE5-CPcB-AFh.js → pieDiagram-4H26LBE5-BLJ6QnRO.js} +1 -1
  499. package/dist/domains/dashboard/web/assets/{quadrantDiagram-W4KKPZXB-LFy52Qtj.js → quadrantDiagram-W4KKPZXB-CflW-eFH.js} +1 -1
  500. package/dist/domains/dashboard/web/assets/{requirementDiagram-4Y6WPE33-BdjeaYP_.js → requirementDiagram-4Y6WPE33-B9HLmRWH.js} +1 -1
  501. package/dist/domains/dashboard/web/assets/{sankeyDiagram-5OEKKPKP-DDJ4styL.js → sankeyDiagram-5OEKKPKP-6xnco5k5.js} +1 -1
  502. package/dist/domains/dashboard/web/assets/{sequenceDiagram-3UESZ5HK-BL5bFNoz.js → sequenceDiagram-3UESZ5HK-Dp1rujNZ.js} +1 -1
  503. package/dist/domains/dashboard/web/assets/{stateDiagram-AJRCARHV-BNUfikui.js → stateDiagram-AJRCARHV-C1Xc90ek.js} +1 -1
  504. package/dist/domains/dashboard/web/assets/stateDiagram-v2-BHNVJYJU-p-qmzXyE.js +1 -0
  505. package/dist/domains/dashboard/web/assets/{timeline-definition-PNZ67QCA--ps2uu6L.js → timeline-definition-PNZ67QCA-g9s-m5Z4.js} +1 -1
  506. package/dist/domains/dashboard/web/assets/{vennDiagram-CIIHVFJN-C2q8q26c.js → vennDiagram-CIIHVFJN-rkEyhyCc.js} +1 -1
  507. package/dist/domains/dashboard/web/assets/{wardleyDiagram-YWT4CUSO-BUH9gXqd.js → wardleyDiagram-YWT4CUSO-CJLrWGXc.js} +1 -1
  508. package/dist/domains/dashboard/web/assets/{xychartDiagram-2RQKCTM6-B-DielEy.js → xychartDiagram-2RQKCTM6-BxJvPNqo.js} +1 -1
  509. package/dist/domains/dashboard/web/index.html +2 -2
  510. package/dist/domains/engine/storage-layout.d.ts +14 -0
  511. package/dist/domains/engine/storage-layout.d.ts.map +1 -0
  512. package/dist/domains/engine/storage-layout.js +32 -0
  513. package/dist/domains/engine/storage-layout.js.map +1 -0
  514. package/dist/domains/engine/storage-run.d.ts +9 -0
  515. package/dist/domains/engine/storage-run.d.ts.map +1 -0
  516. package/dist/domains/engine/storage-run.js +116 -0
  517. package/dist/domains/engine/storage-run.js.map +1 -0
  518. package/dist/domains/factory/package.d.ts.map +1 -1
  519. package/dist/domains/factory/package.js +5 -3
  520. package/dist/domains/factory/package.js.map +1 -1
  521. package/dist/domains/integrations/openspec.d.ts.map +1 -1
  522. package/dist/domains/integrations/openspec.js +8 -6
  523. package/dist/domains/integrations/openspec.js.map +1 -1
  524. package/dist/domains/skill/platform-inspect.d.ts +6 -2
  525. package/dist/domains/skill/platform-inspect.d.ts.map +1 -1
  526. package/dist/domains/skill/platform-inspect.js +52 -8
  527. package/dist/domains/skill/platform-inspect.js.map +1 -1
  528. package/dist/domains/skill/platform-install.d.ts +18 -7
  529. package/dist/domains/skill/platform-install.d.ts.map +1 -1
  530. package/dist/domains/skill/platform-install.js +338 -71
  531. package/dist/domains/skill/platform-install.js.map +1 -1
  532. package/dist/domains/skill/project-instructions.d.ts.map +1 -1
  533. package/dist/domains/skill/project-instructions.js +12 -6
  534. package/dist/domains/skill/project-instructions.js.map +1 -1
  535. package/dist/domains/skill/uninstall.d.ts.map +1 -1
  536. package/dist/domains/skill/uninstall.js +15 -3
  537. package/dist/domains/skill/uninstall.js.map +1 -1
  538. package/dist/domains/workflow-contract/builtins.d.ts +6 -0
  539. package/dist/domains/workflow-contract/builtins.d.ts.map +1 -1
  540. package/dist/domains/workflow-contract/builtins.js +137 -0
  541. package/dist/domains/workflow-contract/builtins.js.map +1 -1
  542. package/dist/domains/workflow-contract/index.d.ts +3 -1
  543. package/dist/domains/workflow-contract/index.d.ts.map +1 -1
  544. package/dist/domains/workflow-contract/index.js +2 -1
  545. package/dist/domains/workflow-contract/index.js.map +1 -1
  546. package/dist/domains/workflow-contract/normalize.d.ts.map +1 -1
  547. package/dist/domains/workflow-contract/normalize.js +28 -10
  548. package/dist/domains/workflow-contract/normalize.js.map +1 -1
  549. package/dist/domains/workflow-contract/project-config.d.ts +7 -0
  550. package/dist/domains/workflow-contract/project-config.d.ts.map +1 -0
  551. package/dist/domains/workflow-contract/project-config.js +110 -0
  552. package/dist/domains/workflow-contract/project-config.js.map +1 -0
  553. package/dist/domains/workflow-contract/types.d.ts +4 -2
  554. package/dist/domains/workflow-contract/types.d.ts.map +1 -1
  555. package/dist/domains/workflow-contract/validation.d.ts.map +1 -1
  556. package/dist/domains/workflow-contract/validation.js +14 -5
  557. package/dist/domains/workflow-contract/validation.js.map +1 -1
  558. package/dist/platform/install/detect.d.ts +3 -1
  559. package/dist/platform/install/detect.d.ts.map +1 -1
  560. package/dist/platform/install/detect.js +4 -1
  561. package/dist/platform/install/detect.js.map +1 -1
  562. package/dist/platform/install/project-registry.js +2 -2
  563. package/dist/platform/install/project-registry.js.map +1 -1
  564. package/dist/platform/paths/repository-layout.d.ts +16 -0
  565. package/dist/platform/paths/repository-layout.d.ts.map +1 -1
  566. package/dist/platform/paths/repository-layout.js.map +1 -1
  567. package/eval/local/README.md +50 -0
  568. package/eval/local/scripts/compare_baselines.py +127 -1
  569. package/eval/local/scripts/regression_check.py +3 -4
  570. package/eval/local/tasks/comet-native-clarification/environment/Dockerfile +27 -0
  571. package/eval/local/tasks/comet-native-clarification/environment/test_wordcount.py +25 -0
  572. package/eval/local/tasks/comet-native-clarification/environment/wordcount.py +36 -0
  573. package/eval/local/tasks/comet-native-clarification/instruction.md +7 -0
  574. package/eval/local/tasks/comet-native-clarification/task.toml +40 -0
  575. package/eval/local/tasks/comet-native-clarification/validation/test_native_clarification.py +193 -0
  576. package/eval/local/tasks/comet-native-clarification-modes/environment/Dockerfile +27 -0
  577. package/eval/local/tasks/comet-native-clarification-modes/environment/test_wordcount.py +25 -0
  578. package/eval/local/tasks/comet-native-clarification-modes/environment/wordcount.py +36 -0
  579. package/eval/local/tasks/comet-native-clarification-modes/instruction.md +13 -0
  580. package/eval/local/tasks/comet-native-clarification-modes/task.toml +39 -0
  581. package/eval/local/tasks/comet-native-clarification-modes/validation/test_native_clarification_modes.py +235 -0
  582. package/eval/local/tasks/comet-native-interrupted-transition/environment/.comet/config.yaml +6 -0
  583. package/eval/local/tasks/comet-native-interrupted-transition/environment/Dockerfile +28 -0
  584. package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/brief.md +17 -0
  585. package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/comet-state.yaml +12 -0
  586. package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/runtime/transition.json +69 -0
  587. package/eval/local/tasks/comet-native-interrupted-transition/environment/docs/comet/changes/add-character-counting/specs/character-counting/spec.md +3 -0
  588. package/eval/local/tasks/comet-native-interrupted-transition/environment/test_wordcount.py +20 -0
  589. package/eval/local/tasks/comet-native-interrupted-transition/environment/wordcount.py +27 -0
  590. package/eval/local/tasks/comet-native-interrupted-transition/instruction.md +7 -0
  591. package/eval/local/tasks/comet-native-interrupted-transition/task.toml +35 -0
  592. package/eval/local/tasks/comet-native-interrupted-transition/validation/test_native_interrupted_transition.py +63 -0
  593. package/eval/local/tasks/comet-native-repository-fact/environment/Dockerfile +26 -0
  594. package/eval/local/tasks/comet-native-repository-fact/environment/PRODUCT_RULES.md +3 -0
  595. package/eval/local/tasks/comet-native-repository-fact/environment/test_wordcount.py +20 -0
  596. package/eval/local/tasks/comet-native-repository-fact/environment/wordcount.py +27 -0
  597. package/eval/local/tasks/comet-native-repository-fact/instruction.md +7 -0
  598. package/eval/local/tasks/comet-native-repository-fact/task.toml +35 -0
  599. package/eval/local/tasks/comet-native-repository-fact/validation/test_native_repository_fact.py +53 -0
  600. package/eval/local/tasks/comet-native-wave-b-decision-resume/environment/Dockerfile +27 -0
  601. package/eval/local/tasks/comet-native-wave-b-decision-resume/environment/test_wordcount.py +27 -0
  602. package/eval/local/tasks/comet-native-wave-b-decision-resume/environment/wordcount.py +36 -0
  603. package/eval/local/tasks/comet-native-wave-b-decision-resume/instruction.md +9 -0
  604. package/eval/local/tasks/comet-native-wave-b-decision-resume/task.toml +40 -0
  605. package/eval/local/tasks/comet-native-wave-b-decision-resume/validation/test_native_wave_b_decision_resume.py +208 -0
  606. package/eval/local/tasks/comet-native-wave-c-verification-integrity/environment/Dockerfile +27 -0
  607. package/eval/local/tasks/comet-native-wave-c-verification-integrity/environment/test_wordcount.py +27 -0
  608. package/eval/local/tasks/comet-native-wave-c-verification-integrity/environment/wordcount.py +36 -0
  609. package/eval/local/tasks/comet-native-wave-c-verification-integrity/instruction.md +11 -0
  610. package/eval/local/tasks/comet-native-wave-c-verification-integrity/task.toml +36 -0
  611. package/eval/local/tasks/comet-native-wave-c-verification-integrity/validation/test_native_wave_c_verification_integrity.py +344 -0
  612. package/eval/local/tasks/comet-native-wave-d-stagnation-stop/environment/Dockerfile +27 -0
  613. package/eval/local/tasks/comet-native-wave-d-stagnation-stop/environment/test_wordcount.py +27 -0
  614. package/eval/local/tasks/comet-native-wave-d-stagnation-stop/environment/wordcount.py +36 -0
  615. package/eval/local/tasks/comet-native-wave-d-stagnation-stop/instruction.md +11 -0
  616. package/eval/local/tasks/comet-native-wave-d-stagnation-stop/task.toml +37 -0
  617. package/eval/local/tasks/comet-native-wave-d-stagnation-stop/validation/test_native_wave_d_stagnation_stop.py +284 -0
  618. package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/.include-trusted-native-runtime +1 -0
  619. package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/Dockerfile +27 -0
  620. package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/test_wordcount.py +27 -0
  621. package/eval/local/tasks/comet-native-wave-e-parallel-safety/environment/wordcount.py +36 -0
  622. package/eval/local/tasks/comet-native-wave-e-parallel-safety/instruction.md +9 -0
  623. package/eval/local/tasks/comet-native-wave-e-parallel-safety/task.toml +37 -0
  624. package/eval/local/tasks/comet-native-wave-e-parallel-safety/validation/test_native_wave_e_parallel_safety.py +309 -0
  625. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/.include-current-comet-cli +1 -0
  626. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/Dockerfile +31 -0
  627. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/current-comet-package.json +18 -0
  628. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/current-comet.sh +14 -0
  629. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/test_wordcount.py +27 -0
  630. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/environment/wordcount.py +36 -0
  631. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/instruction.md +7 -0
  632. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/task.toml +36 -0
  633. package/eval/local/tasks/comet-native-wave-f-dashboard-readonly/validation/test_native_wave_f_dashboard_readonly.py +469 -0
  634. package/eval/local/tasks/comet-native-workflow/environment/Dockerfile +27 -0
  635. package/eval/local/tasks/comet-native-workflow/environment/test_wordcount.py +25 -0
  636. package/eval/local/tasks/comet-native-workflow/environment/wordcount.py +36 -0
  637. package/eval/local/tasks/comet-native-workflow/instruction.md +14 -0
  638. package/eval/local/tasks/comet-native-workflow/task.toml +36 -0
  639. package/eval/local/tasks/comet-native-workflow/validation/test_native_workflow.py +149 -0
  640. package/eval/local/tasks/index.yaml +61 -0
  641. package/eval/local/tests/conftest.py +407 -22
  642. package/eval/local/tests/scaffold/test_aligned_comparison.py +1012 -0
  643. package/eval/local/tests/scaffold/test_conftest_helpers.py +187 -7
  644. package/eval/local/tests/scaffold/test_logging.py +135 -1
  645. package/eval/local/tests/scaffold/test_manifests.py +2 -0
  646. package/eval/local/tests/scaffold/test_native_aligned_eval.py +346 -0
  647. package/eval/local/tests/scaffold/test_native_wave_evaluations.py +1889 -0
  648. package/eval/local/tests/scaffold/test_profiles.py +33 -0
  649. package/eval/local/tests/scaffold/test_sample_quality.py +57 -0
  650. package/eval/local/tests/scaffold/test_tasks.py +377 -1
  651. package/eval/local/tests/scaffold/test_treatments.py +59 -13
  652. package/eval/local/tests/scaffold/test_utils.py +277 -4
  653. package/eval/local/tests/tasks/__init__.py +1 -0
  654. package/eval/local/tests/tasks/test_tasks.py +104 -78
  655. package/eval/local/tests/tasks/test_validation_scripts.py +37 -0
  656. package/eval/local/treatments/comet/comet_native_phase1.yaml +66 -0
  657. package/eval/scaffold/python/aligned_comparison.py +1631 -0
  658. package/eval/scaffold/python/generic_llm_judge.py +3 -1
  659. package/eval/scaffold/python/llm_judge.py +0 -3
  660. package/eval/scaffold/python/logging.py +115 -16
  661. package/eval/scaffold/python/manifests.py +2 -0
  662. package/eval/scaffold/python/native_eval.py +138 -0
  663. package/eval/scaffold/python/pass_at_k.py +0 -1
  664. package/eval/scaffold/python/report_outputs.py +226 -44
  665. package/eval/scaffold/python/sample_quality.py +45 -2
  666. package/eval/scaffold/python/tasks.py +11 -0
  667. package/eval/scaffold/python/utils.py +108 -20
  668. package/eval/scaffold/python/validation/__init__.py +15 -5
  669. package/eval/scaffold/python/validation/comet_workflow.py +10 -5
  670. package/eval/scaffold/python/validation/generic_rubric.py +1 -2
  671. package/eval/scaffold/python/validation/native_wave.py +2154 -0
  672. package/eval/scaffold/python/validation/native_workflow.py +252 -0
  673. package/eval/scaffold/python/validation/rubric.py +0 -2
  674. package/eval/scaffold/shell/completion-point.sh +18 -0
  675. package/eval/scaffold/shell/decision-point.sh +35 -0
  676. package/eval/scaffold/shell/docker.sh +150 -9
  677. package/eval/scaffold/shell/run-claude-loop.sh +57 -27
  678. package/package.json +19 -5
  679. package/dist/domains/dashboard/web/assets/channel-kp6Ng2JJ.js +0 -1
  680. package/dist/domains/dashboard/web/assets/classDiagram-4FO5ZUOK-yBDjgMY_.js +0 -1
  681. package/dist/domains/dashboard/web/assets/classDiagram-v2-Q7XG4LA2-yBDjgMY_.js +0 -1
  682. package/dist/domains/dashboard/web/assets/demo-DQyfozbi.js +0 -1
  683. package/dist/domains/dashboard/web/assets/index-BLAfkNTu.css +0 -1
  684. package/dist/domains/dashboard/web/assets/index-DtLUOcAW.js +0 -24
  685. package/dist/domains/dashboard/web/assets/stateDiagram-v2-BHNVJYJU-DGrsmWts.js +0 -1
  686. package/eval/.env.example +0 -52
  687. package/eval/langsmith/.env.example +0 -16
@@ -0,0 +1,1631 @@
1
+ """Strict, task-macro comparison for two eval experiments.
2
+
3
+ The legacy comparison report pools every run in one experiment. That is useful
4
+ for a quick dashboard, but it is not a valid pass@k comparison when treatments
5
+ were executed in separate experiments or when tasks have different numbers of
6
+ repetitions. This module pairs runs by ``(task, repetition, case hash)`` and
7
+ then computes each task's metric before taking the macro average.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import re
16
+ import stat
17
+ import statistics
18
+ from collections import Counter, defaultdict
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Any, Iterable, Sequence
22
+
23
+ from scaffold.python.logging import extract_events, parse_output
24
+ from scaffold.python.pass_at_k import compute_pass_metrics
25
+ from scaffold.python.sample_quality import quality_from_report
26
+
27
+
28
+ CASE_MANIFEST_SCHEMA_V1 = "comet.eval.case-manifest.v1"
29
+ CASE_MANIFEST_SCHEMA = "comet.eval.case-manifest.v2"
30
+ EXECUTION_IDENTITY_SCHEMA = "comet.eval.execution-identity.v1"
31
+ EXPECTED_CASE_MATRIX_SCHEMA = "comet.eval.expected-case-matrix.v1"
32
+ EXPECTED_CASE_MATRIX_FILENAME = "expected-case-matrix.json"
33
+ DURATION_SOURCE = "raw_stdout:extract_events"
34
+ MAX_RAW_STDOUT_BYTES = 64 * 1024 * 1024
35
+ MAX_EXPECTED_CASE_MATRIX_BYTES = 8 * 1024 * 1024
36
+ _EVAL_ROOT = Path(__file__).resolve().parents[2]
37
+ _REPORT_SUFFIX_RE = r"(?:-r(?P<rep>\d+))?$"
38
+ _CASE_IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,199}")
39
+ _HASHED_CASE_PARTS_V1 = (
40
+ "task_hash",
41
+ "instruction_hash",
42
+ "validator_hash",
43
+ "environment_hash",
44
+ "data_hash",
45
+ "prompt_hash",
46
+ )
47
+ _HASHED_CASE_PARTS_V2 = (
48
+ *_HASHED_CASE_PARTS_V1,
49
+ "runner_hash",
50
+ "controller_hash",
51
+ "execution_hash",
52
+ )
53
+ _EXECUTION_IDENTITY_HASH_KEYS = (
54
+ "image_id_hash",
55
+ "image_repo_digests_hash",
56
+ "image_ref_hash",
57
+ "claude_tool_version_hash",
58
+ "model_selection_hash",
59
+ "interaction_hash",
60
+ )
61
+ _RUNNER_FILES = (
62
+ "scaffold/shell/docker.sh",
63
+ "scaffold/shell/common.sh",
64
+ "scaffold/shell/run-claude-loop.sh",
65
+ "scaffold/shell/decision-point.sh",
66
+ "scaffold/shell/completion-point.sh",
67
+ )
68
+ _CONTROLLER_FILES = (
69
+ "local/tests/tasks/test_tasks.py",
70
+ "local/tests/conftest.py",
71
+ "scaffold/python/tasks.py",
72
+ "scaffold/python/treatments.py",
73
+ "scaffold/python/profiles.py",
74
+ "scaffold/python/manifests.py",
75
+ "scaffold/python/utils.py",
76
+ "scaffold/python/logging.py",
77
+ "scaffold/python/aligned_comparison.py",
78
+ )
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class CaseManifest:
83
+ schema: str
84
+ task: str
85
+ case_hash: str
86
+ core_hash: str
87
+ task_hash: str
88
+ instruction_hash: str
89
+ validator_hash: str
90
+ environment_hash: str
91
+ data_hash: str
92
+ prompt_hash: str
93
+ runner_hash: str | None
94
+ controller_hash: str | None
95
+ execution_hash: str | None
96
+ execution_identity: dict[str, str] | None
97
+ source: str
98
+ run_bound: bool
99
+ execution_bound: bool
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class ExpectedCase:
104
+ task: str
105
+ treatment: str
106
+ rep: int
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class ExpectedCaseMatrix:
111
+ matrix_hash: str
112
+ cases: tuple[ExpectedCase, ...]
113
+
114
+ def keys_for(self, treatment: str) -> frozenset[tuple[str, int]]:
115
+ return frozenset(
116
+ (case.task, case.rep) for case in self.cases if case.treatment == treatment
117
+ )
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class ExpectedCaseMatrixLoad:
122
+ matrix: ExpectedCaseMatrix | None
123
+ error: str | None
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class RunRecord:
128
+ task: str
129
+ rep: int
130
+ treatment: str
131
+ report: dict[str, Any]
132
+ report_path: Path
133
+ manifest: CaseManifest
134
+ manifest_valid: bool
135
+ include_in_analysis: bool
136
+ quality_reason: str
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class AlignmentIssue:
141
+ task: str
142
+ rep: int
143
+ reason: str
144
+ detail: str
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class AlignedPair:
149
+ task: str
150
+ rep: int
151
+ case_hash: str
152
+ candidate: RunRecord
153
+ baseline: RunRecord
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class AlignmentResult:
158
+ candidate_records: tuple[RunRecord, ...]
159
+ baseline_records: tuple[RunRecord, ...]
160
+ pairs: tuple[AlignedPair, ...]
161
+ issues: tuple[AlignmentIssue, ...]
162
+ tasks: tuple[str, ...]
163
+ expected_keys: int
164
+
165
+
166
+ def _sha256_bytes(value: bytes) -> str:
167
+ return "sha256:" + hashlib.sha256(value).hexdigest()
168
+
169
+
170
+ def _hash_tree(path: Path) -> str:
171
+ """Hash a file/tree without following symlinks."""
172
+ digest = hashlib.sha256()
173
+ if not path.exists() and not path.is_symlink():
174
+ digest.update(b"missing\0")
175
+ return "sha256:" + digest.hexdigest()
176
+
177
+ paths = [path] if path.is_file() or path.is_symlink() else sorted(path.rglob("*"))
178
+ for item in paths:
179
+ if item.name.startswith(".env"):
180
+ continue
181
+ if item.name == "__pycache__" or "__pycache__" in item.parts:
182
+ continue
183
+ if item.suffix in {".pyc", ".pyo"}:
184
+ continue
185
+ relative = item.name if item == path else item.relative_to(path).as_posix()
186
+ if item.is_symlink():
187
+ digest.update(f"L\0{relative}\0{item.readlink()}\0".encode("utf-8"))
188
+ elif item.is_file():
189
+ digest.update(f"F\0{relative}\0".encode("utf-8"))
190
+ digest.update(item.read_bytes())
191
+ digest.update(b"\0")
192
+ return "sha256:" + digest.hexdigest()
193
+
194
+
195
+ def _case_hash(task: str, components: dict[str, str], *, schema: str) -> str:
196
+ payload = {
197
+ "schema": schema,
198
+ "task": task,
199
+ **components,
200
+ }
201
+ return _sha256_bytes(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8"))
202
+
203
+
204
+ def _case_core_hash(task: str, components: dict[str, str]) -> str:
205
+ return _case_hash(
206
+ task,
207
+ {key: components[key] for key in _HASHED_CASE_PARTS_V1},
208
+ schema="comet.eval.case-core.v1",
209
+ )
210
+
211
+
212
+ def _hash_named_files(paths: Sequence[str]) -> str:
213
+ components = {path: _hash_tree(_EVAL_ROOT / path) for path in paths}
214
+ return _sha256_bytes(
215
+ json.dumps(components, sort_keys=True, separators=(",", ":")).encode("utf-8")
216
+ )
217
+
218
+
219
+ def _is_hash(value: Any) -> bool:
220
+ return isinstance(value, str) and re.fullmatch(r"sha256:[0-9a-f]{64}", value) is not None
221
+
222
+
223
+ def _hash_private_value(value: Any) -> str:
224
+ """Hash configuration without persisting its potentially sensitive text."""
225
+ return _sha256_bytes(
226
+ json.dumps(value, sort_keys=True, separators=(",", ":"), default=str).encode("utf-8")
227
+ )
228
+
229
+
230
+ def build_execution_identity(
231
+ docker_identity: dict[str, Any],
232
+ *,
233
+ model: str | None,
234
+ model_config: dict[str, str | None] | None = None,
235
+ interaction: Any,
236
+ ) -> dict[str, str]:
237
+ """Build the safe, report-bound execution identity.
238
+
239
+ Docker supplies hashes derived from the immutable image and the actual
240
+ ``claude --version`` output. Model and interaction text are hashed by the
241
+ controller; only bounded enums and digests enter reports.
242
+ """
243
+ if docker_identity.get("schema") != EXECUTION_IDENTITY_SCHEMA:
244
+ raise ValueError("Docker execution identity schema is invalid")
245
+ allowed = {"schema", "runtime_image_id", *_EXECUTION_IDENTITY_HASH_KEYS[:4]}
246
+ unknown = set(docker_identity) - allowed
247
+ if unknown:
248
+ raise ValueError(f"Docker execution identity has unknown keys: {sorted(unknown)}")
249
+ runtime_image_id = docker_identity.get("runtime_image_id")
250
+ if not isinstance(runtime_image_id, str) or not re.fullmatch(
251
+ r"sha256:[0-9a-f]{64}", runtime_image_id
252
+ ):
253
+ raise ValueError("Docker execution identity does not include an immutable image ID")
254
+ for key in _EXECUTION_IDENTITY_HASH_KEYS[:4]:
255
+ if not _is_hash(docker_identity.get(key)):
256
+ raise ValueError(f"Docker execution identity {key} is invalid")
257
+ if docker_identity["image_id_hash"] != _sha256_bytes(runtime_image_id.encode("utf-8")):
258
+ raise ValueError("Docker execution identity image hash does not match its runtime image")
259
+
260
+ mode = getattr(interaction, "mode", None) or "none"
261
+ if mode not in {"none", "auto_user"}:
262
+ mode = "custom"
263
+ max_turns = getattr(interaction, "max_turns", None)
264
+ if isinstance(max_turns, bool) or not isinstance(max_turns, int) or not 1 <= max_turns <= 1000:
265
+ raise ValueError("Interaction max_turns must be between 1 and 1000")
266
+ interaction_payload = {
267
+ "mode": mode,
268
+ "max_turns": max_turns,
269
+ "simulator_prompt": getattr(interaction, "simulator_prompt", None),
270
+ "decision_patterns": list(getattr(interaction, "decision_patterns", ()) or ()),
271
+ "decision_reply": getattr(interaction, "decision_reply", None),
272
+ "continue_prompt": getattr(interaction, "continue_prompt", None),
273
+ "fresh_resume_marker": getattr(interaction, "fresh_resume_marker", None),
274
+ }
275
+ model_source = "explicit" if model else "runtime-default"
276
+ normalized_model_config = {
277
+ key: value
278
+ for key, value in sorted((model_config or {}).items())
279
+ if isinstance(key, str) and isinstance(value, (str, type(None)))
280
+ }
281
+ return {
282
+ "schema": EXECUTION_IDENTITY_SCHEMA,
283
+ **{key: docker_identity[key] for key in _EXECUTION_IDENTITY_HASH_KEYS[:4]},
284
+ "model_selection_hash": _hash_private_value(
285
+ {
286
+ "source": model_source,
287
+ "selection": model or "runtime-default",
288
+ "runtime_config": normalized_model_config,
289
+ }
290
+ ),
291
+ "interaction_hash": _hash_private_value(interaction_payload),
292
+ "model_source": model_source,
293
+ "interaction_mode": mode,
294
+ "interaction_max_turns": str(max_turns),
295
+ }
296
+
297
+
298
+ def _validated_execution_identity(raw: Any) -> dict[str, str] | None:
299
+ if not isinstance(raw, dict):
300
+ return None
301
+ allowed = {
302
+ "schema",
303
+ *_EXECUTION_IDENTITY_HASH_KEYS,
304
+ "model_source",
305
+ "interaction_mode",
306
+ "interaction_max_turns",
307
+ }
308
+ if set(raw) != allowed or raw.get("schema") != EXECUTION_IDENTITY_SCHEMA:
309
+ return None
310
+ if not all(_is_hash(raw.get(key)) for key in _EXECUTION_IDENTITY_HASH_KEYS):
311
+ return None
312
+ if raw.get("model_source") not in {"explicit", "runtime-default"}:
313
+ return None
314
+ if raw.get("interaction_mode") not in {"none", "auto_user", "custom"}:
315
+ return None
316
+ try:
317
+ max_turns = int(raw.get("interaction_max_turns", ""))
318
+ except (TypeError, ValueError):
319
+ return None
320
+ if not 1 <= max_turns <= 1000 or str(max_turns) != raw.get("interaction_max_turns"):
321
+ return None
322
+ return {key: raw[key] for key in sorted(allowed)}
323
+
324
+
325
+ def build_case_manifest(
326
+ task: str,
327
+ tasks_dir: Path,
328
+ *,
329
+ execution_identity: dict[str, str] | None = None,
330
+ ) -> CaseManifest:
331
+ """Build a manifest for the canonical task and, for new runs, its execution."""
332
+ task_dir = tasks_dir / task
333
+ instruction_path = task_dir / "instruction.md"
334
+ task_hash = _hash_tree(task_dir / "task.toml")
335
+ instruction_hash = _hash_tree(instruction_path)
336
+ validator_components = {
337
+ "task_validation": _hash_tree(task_dir / "validation"),
338
+ "shared_validation": _hash_tree(_EVAL_ROOT / "scaffold" / "python" / "validation"),
339
+ "native_adapter": _hash_tree(_EVAL_ROOT / "scaffold" / "python" / "native_eval.py"),
340
+ }
341
+ validator_hash = _sha256_bytes(
342
+ json.dumps(validator_components, sort_keys=True, separators=(",", ":")).encode("utf-8")
343
+ )
344
+ environment_hash = _hash_tree(task_dir / "environment")
345
+ data_hash = _hash_tree(task_dir / "data")
346
+ instruction_bytes = instruction_path.read_bytes() if instruction_path.is_file() else b"missing"
347
+ prompt_components = {
348
+ "instruction": _sha256_bytes(b"canonical-task-prompt-v1\0" + instruction_bytes),
349
+ "task_renderer": _hash_tree(_EVAL_ROOT / "scaffold" / "python" / "tasks.py"),
350
+ "native_adapter": validator_components["native_adapter"],
351
+ }
352
+ prompt_hash = _sha256_bytes(
353
+ json.dumps(prompt_components, sort_keys=True, separators=(",", ":")).encode("utf-8")
354
+ )
355
+ base_components = {
356
+ "task_hash": task_hash,
357
+ "instruction_hash": instruction_hash,
358
+ "validator_hash": validator_hash,
359
+ "environment_hash": environment_hash,
360
+ "data_hash": data_hash,
361
+ "prompt_hash": prompt_hash,
362
+ }
363
+ if execution_identity is None:
364
+ schema = CASE_MANIFEST_SCHEMA_V1
365
+ components = base_components
366
+ runner_hash = controller_hash = execution_hash = None
367
+ validated_identity = None
368
+ source = "current-task-tree-fallback"
369
+ run_bound = False
370
+ execution_bound = False
371
+ else:
372
+ validated_identity = _validated_execution_identity(execution_identity)
373
+ if validated_identity is None:
374
+ raise ValueError("execution identity is not a safe canonical payload")
375
+ schema = CASE_MANIFEST_SCHEMA
376
+ runner_hash = _hash_named_files(_RUNNER_FILES)
377
+ controller_hash = _hash_named_files(_CONTROLLER_FILES)
378
+ execution_hash = _hash_private_value(validated_identity)
379
+ components = {
380
+ **base_components,
381
+ "runner_hash": runner_hash,
382
+ "controller_hash": controller_hash,
383
+ "execution_hash": execution_hash,
384
+ }
385
+ source = "report-bound-v2"
386
+ run_bound = True
387
+ execution_bound = True
388
+ case_hash = _case_hash(task, components, schema=schema)
389
+ core_hash = _case_core_hash(task, base_components)
390
+ return CaseManifest(
391
+ schema=schema,
392
+ task=task,
393
+ case_hash=case_hash,
394
+ core_hash=core_hash,
395
+ task_hash=task_hash,
396
+ instruction_hash=instruction_hash,
397
+ validator_hash=validator_hash,
398
+ environment_hash=environment_hash,
399
+ data_hash=data_hash,
400
+ prompt_hash=prompt_hash,
401
+ runner_hash=runner_hash,
402
+ controller_hash=controller_hash,
403
+ execution_hash=execution_hash,
404
+ execution_identity=validated_identity,
405
+ source=source,
406
+ run_bound=run_bound,
407
+ execution_bound=execution_bound,
408
+ )
409
+
410
+
411
+ def case_manifest_payload(manifest: CaseManifest) -> dict[str, Any]:
412
+ """Serialize a case manifest into the report-bound wire format."""
413
+ payload: dict[str, Any] = {
414
+ "schema": manifest.schema,
415
+ "task": manifest.task,
416
+ "case_hash": manifest.case_hash,
417
+ **{key: getattr(manifest, key) for key in _HASHED_CASE_PARTS_V1},
418
+ }
419
+ if manifest.schema == CASE_MANIFEST_SCHEMA:
420
+ payload.update(
421
+ {
422
+ key: getattr(manifest, key)
423
+ for key in ("runner_hash", "controller_hash", "execution_hash")
424
+ }
425
+ )
426
+ payload["execution_identity"] = manifest.execution_identity
427
+ return payload
428
+
429
+
430
+ def _embedded_case_manifest(report: dict[str, Any], task: str) -> CaseManifest | None:
431
+ events = report.get("events_summary") or {}
432
+ raw = report.get("case_manifest") or events.get("case_manifest")
433
+ if not isinstance(raw, dict):
434
+ return None
435
+ schema = raw.get("schema")
436
+ if schema not in {CASE_MANIFEST_SCHEMA_V1, CASE_MANIFEST_SCHEMA} or raw.get("task") != task:
437
+ return None
438
+ hashed_parts = (
439
+ _HASHED_CASE_PARTS_V2 if schema == CASE_MANIFEST_SCHEMA else _HASHED_CASE_PARTS_V1
440
+ )
441
+ expected_keys = {"schema", "task", "case_hash", *hashed_parts}
442
+ if schema == CASE_MANIFEST_SCHEMA:
443
+ expected_keys.add("execution_identity")
444
+ if set(raw) != expected_keys:
445
+ return None
446
+ if not all(_is_hash(raw.get(key)) for key in hashed_parts):
447
+ return None
448
+ case_hash = raw.get("case_hash")
449
+ if not isinstance(case_hash, str) or not case_hash.startswith("sha256:"):
450
+ return None
451
+ components = {key: raw[key] for key in hashed_parts}
452
+ if case_hash != _case_hash(task, components, schema=schema):
453
+ return None
454
+ execution_identity = (
455
+ _validated_execution_identity(raw.get("execution_identity"))
456
+ if schema == CASE_MANIFEST_SCHEMA
457
+ else None
458
+ )
459
+ if schema == CASE_MANIFEST_SCHEMA and (
460
+ execution_identity is None
461
+ or raw["execution_hash"] != _hash_private_value(execution_identity)
462
+ ):
463
+ return None
464
+ return CaseManifest(
465
+ schema=schema,
466
+ task=task,
467
+ case_hash=case_hash,
468
+ core_hash=_case_core_hash(task, components),
469
+ task_hash=raw["task_hash"],
470
+ instruction_hash=raw["instruction_hash"],
471
+ validator_hash=raw["validator_hash"],
472
+ environment_hash=raw["environment_hash"],
473
+ data_hash=raw["data_hash"],
474
+ prompt_hash=raw["prompt_hash"],
475
+ runner_hash=raw.get("runner_hash"),
476
+ controller_hash=raw.get("controller_hash"),
477
+ execution_hash=raw.get("execution_hash"),
478
+ execution_identity=execution_identity,
479
+ source="report-bound-v2" if schema == CASE_MANIFEST_SCHEMA else "report-bound-v1",
480
+ run_bound=True,
481
+ execution_bound=schema == CASE_MANIFEST_SCHEMA,
482
+ )
483
+
484
+
485
+ def expected_case_matrix_payload(
486
+ cases: Iterable[tuple[str, str, int] | ExpectedCase],
487
+ ) -> dict[str, Any]:
488
+ """Create a deterministic, controller-owned expected-case matrix."""
489
+ normalized: set[tuple[str, str, int]] = set()
490
+ for raw in cases:
491
+ if isinstance(raw, ExpectedCase):
492
+ task, treatment, rep = raw.task, raw.treatment, raw.rep
493
+ else:
494
+ task, treatment, rep = raw
495
+ if (
496
+ not isinstance(task, str)
497
+ or _CASE_IDENTIFIER_RE.fullmatch(task) is None
498
+ or not isinstance(treatment, str)
499
+ or _CASE_IDENTIFIER_RE.fullmatch(treatment) is None
500
+ ):
501
+ raise ValueError("Expected case task and treatment must be non-empty strings")
502
+ if isinstance(rep, bool) or not isinstance(rep, int) or rep < 1:
503
+ raise ValueError("Expected case repetition must be a positive integer")
504
+ key = (task, treatment, rep)
505
+ if key in normalized:
506
+ raise ValueError("Expected case matrix contains a duplicate case")
507
+ normalized.add(key)
508
+ serialized = [
509
+ {"task": task, "treatment": treatment, "rep": rep}
510
+ for task, treatment, rep in sorted(normalized)
511
+ ]
512
+ hash_payload = {"schema": EXPECTED_CASE_MATRIX_SCHEMA, "cases": serialized}
513
+ matrix_hash = _sha256_bytes(
514
+ json.dumps(hash_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
515
+ )
516
+ return {**hash_payload, "matrix_hash": matrix_hash}
517
+
518
+
519
+ def parse_expected_case_matrix(raw: Any) -> ExpectedCaseMatrix:
520
+ if not isinstance(raw, dict) or set(raw) != {"schema", "matrix_hash", "cases"}:
521
+ raise ValueError("expected case matrix has an invalid object shape")
522
+ if raw.get("schema") != EXPECTED_CASE_MATRIX_SCHEMA:
523
+ raise ValueError("expected case matrix schema is invalid")
524
+ cases_raw = raw.get("cases")
525
+ if not isinstance(cases_raw, list):
526
+ raise ValueError("expected case matrix cases must be a list")
527
+ cases: list[ExpectedCase] = []
528
+ seen: set[tuple[str, str, int]] = set()
529
+ for item in cases_raw:
530
+ if not isinstance(item, dict) or set(item) != {"task", "treatment", "rep"}:
531
+ raise ValueError("expected case matrix contains an invalid case")
532
+ task = item.get("task")
533
+ treatment = item.get("treatment")
534
+ rep = item.get("rep")
535
+ if (
536
+ not isinstance(task, str)
537
+ or _CASE_IDENTIFIER_RE.fullmatch(task) is None
538
+ or not isinstance(treatment, str)
539
+ or _CASE_IDENTIFIER_RE.fullmatch(treatment) is None
540
+ ):
541
+ raise ValueError("expected case matrix contains an invalid task or treatment")
542
+ if isinstance(rep, bool) or not isinstance(rep, int) or rep < 1:
543
+ raise ValueError("expected case matrix contains an invalid repetition")
544
+ key = (task, treatment, rep)
545
+ if key in seen:
546
+ raise ValueError("expected case matrix contains a duplicate case")
547
+ seen.add(key)
548
+ cases.append(ExpectedCase(task=task, treatment=treatment, rep=rep))
549
+ if [(case.task, case.treatment, case.rep) for case in cases] != sorted(seen):
550
+ raise ValueError("expected case matrix cases are not canonically ordered")
551
+ canonical = expected_case_matrix_payload(cases)
552
+ if raw.get("matrix_hash") != canonical["matrix_hash"]:
553
+ raise ValueError("expected case matrix hash does not match its cases")
554
+ return ExpectedCaseMatrix(matrix_hash=canonical["matrix_hash"], cases=tuple(cases))
555
+
556
+
557
+ def load_expected_case_matrix(experiment_dir: Path) -> ExpectedCaseMatrixLoad:
558
+ path = experiment_dir / EXPECTED_CASE_MATRIX_FILENAME
559
+ try:
560
+ before = path.lstat()
561
+ except FileNotFoundError:
562
+ return ExpectedCaseMatrixLoad(matrix=None, error=None)
563
+ except OSError:
564
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix metadata cannot be read")
565
+ if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
566
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix is not a regular file")
567
+ if before.st_size > MAX_EXPECTED_CASE_MATRIX_BYTES:
568
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix exceeds the size limit")
569
+ try:
570
+ raw = json.loads(path.read_text(encoding="utf-8"))
571
+ after = path.lstat()
572
+ if _path_identity(before) != _path_identity(after):
573
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix changed during read")
574
+ return ExpectedCaseMatrixLoad(matrix=parse_expected_case_matrix(raw), error=None)
575
+ except OSError:
576
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix cannot be read")
577
+ except UnicodeError:
578
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix is not valid UTF-8")
579
+ except json.JSONDecodeError:
580
+ return ExpectedCaseMatrixLoad(matrix=None, error="matrix is not valid JSON")
581
+ except ValueError as error:
582
+ return ExpectedCaseMatrixLoad(matrix=None, error=str(error))
583
+
584
+
585
+ def _report_identity(
586
+ report: dict[str, Any],
587
+ treatment: str,
588
+ report_path: Path | None = None,
589
+ ) -> tuple[str, int] | None:
590
+ name = str(report.get("name") or "")
591
+ match = re.match(
592
+ rf"^(?P<task>.+)-{re.escape(treatment)}{_REPORT_SUFFIX_RE}",
593
+ name,
594
+ )
595
+ if not match:
596
+ return None
597
+ combined_sample = (
598
+ re.search(r"_sample(?P<rep>\d+)_from_", report_path.stem)
599
+ if report_path is not None
600
+ else None
601
+ )
602
+ rep_text = combined_sample.group("rep") if combined_sample else match.group("rep")
603
+ raw_rep = rep_text if rep_text is not None else report.get("rep", 1)
604
+ if isinstance(raw_rep, bool):
605
+ return None
606
+ try:
607
+ rep = int(raw_rep)
608
+ except (TypeError, ValueError):
609
+ return None
610
+ if rep < 1:
611
+ return None
612
+ return match.group("task"), rep
613
+
614
+
615
+ def load_treatment_records(
616
+ experiment_dir: Path,
617
+ treatment: str,
618
+ *,
619
+ tasks_dir: Path,
620
+ ) -> tuple[RunRecord, ...]:
621
+ reports_dir = experiment_dir / "reports"
622
+ manifests: dict[str, CaseManifest] = {}
623
+ records: list[RunRecord] = []
624
+ if not reports_dir.is_dir():
625
+ return ()
626
+ for path in sorted(reports_dir.glob("*.json")):
627
+ try:
628
+ report = json.loads(path.read_text(encoding="utf-8"))
629
+ except (OSError, json.JSONDecodeError):
630
+ continue
631
+ identity = _report_identity(report, treatment, path)
632
+ if identity is None:
633
+ continue
634
+ task, rep = identity
635
+ events = report.get("events_summary") or {}
636
+ raw_manifest = report.get("case_manifest") or events.get("case_manifest")
637
+ manifest = _embedded_case_manifest(report, task)
638
+ manifest_valid = manifest is not None or raw_manifest is None
639
+ if manifest is None:
640
+ manifest = manifests.setdefault(task, build_case_manifest(task, tasks_dir))
641
+ quality = quality_from_report(report, experiment_dir=experiment_dir)
642
+ records.append(
643
+ RunRecord(
644
+ task=task,
645
+ rep=rep,
646
+ treatment=treatment,
647
+ report=report,
648
+ report_path=path,
649
+ manifest=manifest,
650
+ manifest_valid=manifest_valid,
651
+ include_in_analysis=quality.include_in_analysis,
652
+ quality_reason=quality.reason_code,
653
+ )
654
+ )
655
+ return tuple(records)
656
+
657
+
658
+ def align_records(
659
+ candidate_records: Sequence[RunRecord],
660
+ baseline_records: Sequence[RunRecord],
661
+ *,
662
+ candidate_expected: frozenset[tuple[str, int]] | None = None,
663
+ baseline_expected: frozenset[tuple[str, int]] | None = None,
664
+ initial_issues: Sequence[AlignmentIssue] = (),
665
+ ) -> AlignmentResult:
666
+ candidate_by_key: dict[tuple[str, int], list[RunRecord]] = defaultdict(list)
667
+ baseline_by_key: dict[tuple[str, int], list[RunRecord]] = defaultdict(list)
668
+ for record in candidate_records:
669
+ candidate_by_key[(record.task, record.rep)].append(record)
670
+ for record in baseline_records:
671
+ baseline_by_key[(record.task, record.rep)].append(record)
672
+
673
+ candidate_observed = set(candidate_by_key)
674
+ baseline_observed = set(baseline_by_key)
675
+ effective_candidate_expected = (
676
+ candidate_observed if candidate_expected is None else set(candidate_expected)
677
+ )
678
+ effective_baseline_expected = (
679
+ baseline_observed if baseline_expected is None else set(baseline_expected)
680
+ )
681
+ keys = sorted(
682
+ candidate_observed
683
+ | baseline_observed
684
+ | effective_candidate_expected
685
+ | effective_baseline_expected
686
+ )
687
+ tasks = tuple(sorted({task for task, _rep in keys}))
688
+ issues: list[AlignmentIssue] = list(initial_issues)
689
+ pairs: list[AlignedPair] = []
690
+ for task, rep in keys:
691
+ key = (task, rep)
692
+ candidate_matches = candidate_by_key.get((task, rep), [])
693
+ baseline_matches = baseline_by_key.get((task, rep), [])
694
+ matrix_mismatch = (
695
+ candidate_expected is not None
696
+ and baseline_expected is not None
697
+ and (key in effective_candidate_expected) != (key in effective_baseline_expected)
698
+ )
699
+ if matrix_mismatch:
700
+ omitted = (
701
+ "baseline matrix omits case"
702
+ if key in effective_candidate_expected
703
+ else "candidate matrix omits case"
704
+ )
705
+ issues.append(AlignmentIssue(task, rep, "expected-matrix-mismatch", omitted))
706
+ unexpected: list[str] = []
707
+ if candidate_expected is not None and candidate_matches and key not in candidate_expected:
708
+ unexpected.append("candidate")
709
+ if baseline_expected is not None and baseline_matches and key not in baseline_expected:
710
+ unexpected.append("baseline")
711
+ if unexpected:
712
+ issues.append(AlignmentIssue(task, rep, "unexpected-run", ", ".join(unexpected)))
713
+ continue
714
+ if len(candidate_matches) != 1 or len(baseline_matches) != 1:
715
+ if len(candidate_matches) > 1 or len(baseline_matches) > 1:
716
+ detail = f"candidate={len(candidate_matches)}, baseline={len(baseline_matches)}"
717
+ issues.append(AlignmentIssue(task, rep, "duplicate-run", detail))
718
+ elif not candidate_matches or not baseline_matches:
719
+ missing = []
720
+ if not candidate_matches:
721
+ missing.append("candidate")
722
+ if not baseline_matches:
723
+ missing.append("baseline")
724
+ issues.append(
725
+ AlignmentIssue(
726
+ task,
727
+ rep,
728
+ "missing-repetition",
729
+ f"missing {' and '.join(missing)} run",
730
+ )
731
+ )
732
+ continue
733
+
734
+ candidate = candidate_matches[0]
735
+ baseline = baseline_matches[0]
736
+ invalid_manifests = []
737
+ if not candidate.manifest_valid:
738
+ invalid_manifests.append("candidate")
739
+ if not baseline.manifest_valid:
740
+ invalid_manifests.append("baseline")
741
+ if invalid_manifests:
742
+ issues.append(
743
+ AlignmentIssue(
744
+ task,
745
+ rep,
746
+ "invalid-case-manifest",
747
+ ", ".join(invalid_manifests),
748
+ )
749
+ )
750
+ continue
751
+ excluded = []
752
+ if not candidate.include_in_analysis:
753
+ excluded.append(f"candidate:{candidate.quality_reason}")
754
+ if not baseline.include_in_analysis:
755
+ excluded.append(f"baseline:{baseline.quality_reason}")
756
+ if excluded:
757
+ issues.append(AlignmentIssue(task, rep, "quality-excluded", ", ".join(excluded)))
758
+ continue
759
+ exact_execution_identity = (
760
+ candidate.manifest.execution_bound and baseline.manifest.execution_bound
761
+ )
762
+ comparison_hash = (
763
+ candidate.manifest.case_hash
764
+ if exact_execution_identity
765
+ else candidate.manifest.core_hash
766
+ )
767
+ baseline_comparison_hash = (
768
+ baseline.manifest.case_hash if exact_execution_identity else baseline.manifest.core_hash
769
+ )
770
+ if comparison_hash != baseline_comparison_hash:
771
+ issues.append(
772
+ AlignmentIssue(
773
+ task,
774
+ rep,
775
+ "case-hash-mismatch",
776
+ f"candidate={comparison_hash}, baseline={baseline_comparison_hash}",
777
+ )
778
+ )
779
+ continue
780
+ pairs.append(
781
+ AlignedPair(
782
+ task=task,
783
+ rep=rep,
784
+ case_hash=comparison_hash,
785
+ candidate=candidate,
786
+ baseline=baseline,
787
+ )
788
+ )
789
+
790
+ case_hashes_by_task: dict[str, set[str]] = defaultdict(set)
791
+ for pair in pairs:
792
+ case_hashes_by_task[pair.task].add(pair.case_hash)
793
+ drifting_tasks = {
794
+ task: hashes for task, hashes in case_hashes_by_task.items() if len(hashes) > 1
795
+ }
796
+ if drifting_tasks:
797
+ pairs = [pair for pair in pairs if pair.task not in drifting_tasks]
798
+ for task, hashes in sorted(drifting_tasks.items()):
799
+ issues.append(
800
+ AlignmentIssue(
801
+ task,
802
+ 0,
803
+ "case-hash-drift",
804
+ ", ".join(sorted(hashes)),
805
+ )
806
+ )
807
+
808
+ return AlignmentResult(
809
+ candidate_records=tuple(candidate_records),
810
+ baseline_records=tuple(baseline_records),
811
+ pairs=tuple(pairs),
812
+ issues=tuple(issues),
813
+ tasks=tasks,
814
+ expected_keys=len(keys),
815
+ )
816
+
817
+
818
+ def _run_passed(report: dict[str, Any]) -> bool:
819
+ failed = report.get("checks_failed")
820
+ return report.get("passed") is True and isinstance(failed, list) and len(failed) == 0
821
+
822
+
823
+ def task_macro_pass_metrics(
824
+ results_by_task: dict[str, Sequence[bool]],
825
+ ks: Sequence[int],
826
+ *,
827
+ total_tasks: int,
828
+ ) -> dict[int, dict[str, float | int | None]]:
829
+ """Compute task-level pass metrics and macro-average eligible tasks.
830
+
831
+ A task with fewer than ``k`` aligned repetitions is not silently evaluated
832
+ at a smaller k. It is excluded for that k and reported through coverage.
833
+ """
834
+ output: dict[int, dict[str, float | int | None]] = {}
835
+ for k in ks:
836
+ per_task = [
837
+ compute_pass_metrics(values, k)
838
+ for values in results_by_task.values()
839
+ if len(values) >= k
840
+ ]
841
+ output[k] = {
842
+ "pass_at_k": (
843
+ statistics.fmean(float(item["pass_at_k"]) for item in per_task)
844
+ if per_task
845
+ else None
846
+ ),
847
+ "pass_pow_k": (
848
+ statistics.fmean(float(item["pass_pow_k"]) for item in per_task)
849
+ if per_task
850
+ else None
851
+ ),
852
+ "eligible_tasks": len(per_task),
853
+ "total_tasks": total_tasks,
854
+ }
855
+ return output
856
+
857
+
858
+ def _artifact_path(
859
+ report: dict[str, Any],
860
+ experiment_dir: Path,
861
+ key: str,
862
+ *,
863
+ task: str,
864
+ treatment: str,
865
+ rep: int,
866
+ report_path: Path,
867
+ ) -> Path | None:
868
+ reference = ((report.get("events_summary") or {}).get("artifact_references") or {}).get(key)
869
+ if not isinstance(reference, str) or not reference:
870
+ return None
871
+ comparison_root = experiment_dir.resolve()
872
+ raw_path = Path(reference)
873
+ candidate = Path(
874
+ os.path.abspath(raw_path if raw_path.is_absolute() else comparison_root / raw_path)
875
+ )
876
+ allowed_root = comparison_root
877
+ try:
878
+ candidate.relative_to(allowed_root)
879
+ except ValueError:
880
+ combined = re.search(
881
+ r"_sample(?P<rep>\d+)_from_(?P<source>[A-Za-z0-9._-]+)$",
882
+ report_path.stem,
883
+ )
884
+ if combined is None or int(combined.group("rep")) != rep:
885
+ return None
886
+ source = combined.group("source")
887
+ if source in {".", ".."}:
888
+ return None
889
+ allowed_root = comparison_root.parent / source
890
+ try:
891
+ candidate.relative_to(allowed_root)
892
+ except ValueError:
893
+ return None
894
+
895
+ report_name = report.get("name")
896
+ report_rep = report.get("rep", 1)
897
+ if not isinstance(report_name, str) or not report_name:
898
+ return None
899
+ if isinstance(report_rep, bool):
900
+ return None
901
+ try:
902
+ raw_rep = int(report_rep)
903
+ except (TypeError, ValueError):
904
+ return None
905
+ if raw_rep < 1:
906
+ return None
907
+ identity = _report_identity(report, treatment, report_path)
908
+ if identity != (task, rep):
909
+ return None
910
+ expected_names = {
911
+ f"{report_name}_rep{raw_rep}_stdout.json",
912
+ f"{report_name.replace('-', '_')}_rep{raw_rep}_stdout.json",
913
+ }
914
+ if candidate.name not in expected_names:
915
+ return None
916
+ try:
917
+ root_stat = allowed_root.lstat()
918
+ if stat.S_ISLNK(root_stat.st_mode) or not stat.S_ISDIR(root_stat.st_mode):
919
+ return None
920
+ except OSError:
921
+ return None
922
+ return candidate
923
+
924
+
925
+ def _path_identity(value: os.stat_result) -> tuple[int, int, int, int]:
926
+ return (value.st_dev, value.st_ino, value.st_mtime_ns, value.st_size)
927
+
928
+
929
+ def _directory_chain(root: Path, parent: Path) -> tuple[tuple[Path, os.stat_result], ...]:
930
+ relative = parent.relative_to(root)
931
+ paths = [root]
932
+ cursor = root
933
+ for part in relative.parts:
934
+ cursor = cursor / part
935
+ paths.append(cursor)
936
+ chain: list[tuple[Path, os.stat_result]] = []
937
+ for path in paths:
938
+ current = path.lstat()
939
+ if stat.S_ISLNK(current.st_mode) or not stat.S_ISDIR(current.st_mode):
940
+ raise OSError(f"raw stdout parent is not a real directory: {path}")
941
+ chain.append((path, current))
942
+ return tuple(chain)
943
+
944
+
945
+ def _verify_directory_chain(chain: Sequence[tuple[Path, os.stat_result]]) -> None:
946
+ for path, expected in chain:
947
+ current = path.lstat()
948
+ if (
949
+ stat.S_ISLNK(current.st_mode)
950
+ or not stat.S_ISDIR(current.st_mode)
951
+ or _path_identity(current)[:3] != _path_identity(expected)[:3]
952
+ ):
953
+ raise OSError(f"raw stdout parent changed during read: {path}")
954
+
955
+
956
+ def _read_raw_stdout(path: Path, allowed_root: Path) -> str | None:
957
+ try:
958
+ root = allowed_root.resolve(strict=True)
959
+ chain = _directory_chain(root, path.parent)
960
+ before = path.lstat()
961
+ if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
962
+ return None
963
+ if before.st_size > MAX_RAW_STDOUT_BYTES:
964
+ return None
965
+ _verify_directory_chain(chain)
966
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
967
+ descriptor = os.open(path, flags)
968
+ try:
969
+ opened = os.fstat(descriptor)
970
+ after_open = path.lstat()
971
+ if (
972
+ not stat.S_ISREG(opened.st_mode)
973
+ or stat.S_ISLNK(after_open.st_mode)
974
+ or not stat.S_ISREG(after_open.st_mode)
975
+ or _path_identity(opened) != _path_identity(before)
976
+ or _path_identity(after_open) != _path_identity(before)
977
+ ):
978
+ return None
979
+ _verify_directory_chain(chain)
980
+ chunks: list[bytes] = []
981
+ remaining = MAX_RAW_STDOUT_BYTES + 1
982
+ while remaining > 0:
983
+ chunk = os.read(descriptor, min(64 * 1024, remaining))
984
+ if not chunk:
985
+ break
986
+ chunks.append(chunk)
987
+ remaining -= len(chunk)
988
+ if remaining == 0:
989
+ return None
990
+ after_read = os.fstat(descriptor)
991
+ after_path = path.lstat()
992
+ _verify_directory_chain(chain)
993
+ if (
994
+ stat.S_ISLNK(after_path.st_mode)
995
+ or not stat.S_ISREG(after_path.st_mode)
996
+ or _path_identity(after_read) != _path_identity(opened)
997
+ or _path_identity(after_path) != _path_identity(opened)
998
+ ):
999
+ return None
1000
+ return b"".join(chunks).decode("utf-8", errors="replace")
1001
+ finally:
1002
+ os.close(descriptor)
1003
+ except OSError:
1004
+ return None
1005
+
1006
+
1007
+ def recompute_duration(
1008
+ report: dict[str, Any],
1009
+ experiment_dir: Path,
1010
+ *,
1011
+ task: str,
1012
+ treatment: str,
1013
+ rep: int,
1014
+ report_path: Path,
1015
+ ) -> float | None:
1016
+ """Recompute additive duration from raw stdout; never use stale report duration."""
1017
+ telemetry = recompute_telemetry(
1018
+ report,
1019
+ experiment_dir,
1020
+ task=task,
1021
+ treatment=treatment,
1022
+ rep=rep,
1023
+ report_path=report_path,
1024
+ )
1025
+ if telemetry is None:
1026
+ return None
1027
+ duration = telemetry.get("duration_seconds")
1028
+ if isinstance(duration, bool) or not isinstance(duration, (int, float)):
1029
+ return None
1030
+ return float(duration)
1031
+
1032
+
1033
+ def recompute_telemetry(
1034
+ report: dict[str, Any],
1035
+ experiment_dir: Path,
1036
+ *,
1037
+ task: str,
1038
+ treatment: str,
1039
+ rep: int,
1040
+ report_path: Path,
1041
+ ) -> dict[str, Any] | None:
1042
+ """Recompute cumulative task telemetry from the controller-bound raw stdout."""
1043
+ path = _artifact_path(
1044
+ report,
1045
+ experiment_dir,
1046
+ "raw_stdout",
1047
+ task=task,
1048
+ treatment=treatment,
1049
+ rep=rep,
1050
+ report_path=report_path,
1051
+ )
1052
+ if path is None:
1053
+ return None
1054
+ try:
1055
+ path.relative_to(experiment_dir.resolve())
1056
+ allowed_root = experiment_dir
1057
+ except ValueError:
1058
+ combined = re.search(
1059
+ r"_sample(?P<rep>\d+)_from_(?P<source>[A-Za-z0-9._-]+)$",
1060
+ report_path.stem,
1061
+ )
1062
+ if combined is None or int(combined.group("rep")) != rep:
1063
+ return None
1064
+ allowed_root = experiment_dir.resolve().parent / combined.group("source")
1065
+ stdout = _read_raw_stdout(path, allowed_root)
1066
+ if stdout is None:
1067
+ return None
1068
+ telemetry = extract_events(parse_output(stdout))
1069
+ if telemetry.get("subject_invocations") == 0:
1070
+ return None
1071
+ return telemetry
1072
+
1073
+
1074
+ def _hash_short(value: str) -> str:
1075
+ return value.removeprefix("sha256:")[:12]
1076
+
1077
+
1078
+ def _format_metric(value: float | int | None) -> str:
1079
+ return "N/A" if value is None else f"{float(value):.2f}"
1080
+
1081
+
1082
+ def _format_seconds(value: float | None) -> str:
1083
+ return "N/A" if value is None else f"{value:,.0f}s"
1084
+
1085
+
1086
+ def _experiment_label(directory: Path) -> str:
1087
+ """Render a stable public identifier without leaking a controller-local path."""
1088
+ return directory.name
1089
+
1090
+
1091
+ def _manifest_by_task(records: Iterable[RunRecord]) -> dict[str, CaseManifest]:
1092
+ manifests: dict[str, CaseManifest] = {}
1093
+ for record in records:
1094
+ manifests.setdefault(record.task, record.manifest)
1095
+ return manifests
1096
+
1097
+
1098
+ def _results_by_task(
1099
+ pairs: Sequence[AlignedPair],
1100
+ side: str,
1101
+ ) -> dict[str, list[bool]]:
1102
+ results: dict[str, list[bool]] = defaultdict(list)
1103
+ for pair in sorted(pairs, key=lambda item: (item.task, item.rep)):
1104
+ record = pair.candidate if side == "candidate" else pair.baseline
1105
+ results[pair.task].append(_run_passed(record.report))
1106
+ return dict(results)
1107
+
1108
+
1109
+ def _duration_summary(
1110
+ pairs: Sequence[AlignedPair],
1111
+ side: str,
1112
+ experiment_dir: Path,
1113
+ ) -> tuple[int, int, float | None, float | None, tuple[tuple[str, int], ...]]:
1114
+ values: list[float] = []
1115
+ missing: list[tuple[str, int]] = []
1116
+ for pair in pairs:
1117
+ record = pair.candidate if side == "candidate" else pair.baseline
1118
+ duration = recompute_duration(
1119
+ record.report,
1120
+ experiment_dir,
1121
+ task=record.task,
1122
+ treatment=record.treatment,
1123
+ rep=record.rep,
1124
+ report_path=record.report_path,
1125
+ )
1126
+ if duration is not None:
1127
+ values.append(duration)
1128
+ else:
1129
+ missing.append((pair.task, pair.rep))
1130
+ total = sum(values) if values else None
1131
+ average = statistics.fmean(values) if values else None
1132
+ return len(pairs), len(values), total, average, tuple(missing)
1133
+
1134
+
1135
+ _EFFICIENCY_METRICS = (
1136
+ ("subject_invocations", "Model starts/resumes", "count"),
1137
+ ("num_turns", "Agent turns", "count"),
1138
+ ("tool_calls", "Tool calls", "tool-count"),
1139
+ ("duration_seconds", "Wall duration", "seconds"),
1140
+ ("input_tokens", "Non-cache input tokens", "tokens"),
1141
+ ("output_tokens", "Output tokens", "tokens"),
1142
+ ("cache_read_input_tokens", "Cache-read input tokens", "tokens"),
1143
+ ("total_tokens", "Total tokens incl. cache", "tokens"),
1144
+ ("total_cost_usd", "Model cost", "usd"),
1145
+ ("peak_context_input_tokens", "Peak context input", "tokens"),
1146
+ ("p95_context_input_tokens", "P95 context input", "tokens"),
1147
+ ("peak_context_occupancy_pct", "Peak context occupancy", "percent"),
1148
+ )
1149
+
1150
+
1151
+ def _telemetry_value(events: dict[str, Any], key: str, kind: str) -> float | None:
1152
+ value: Any = events.get(key)
1153
+ if kind == "tool-count":
1154
+ value = len(value) if isinstance(value, list) else value
1155
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
1156
+ return None
1157
+ return float(value)
1158
+
1159
+
1160
+ def _format_efficiency(value: float | None, kind: str) -> str:
1161
+ if value is None:
1162
+ return "N/A"
1163
+ if kind == "seconds":
1164
+ return f"{value:,.0f}s"
1165
+ if kind == "tokens":
1166
+ return f"{value:,.0f}"
1167
+ if kind == "usd":
1168
+ return f"${value:,.3f}"
1169
+ if kind == "percent":
1170
+ return f"{value:,.1f}%"
1171
+ return f"{value:,.2f}"
1172
+
1173
+
1174
+ def _format_reduction(candidate: float | None, baseline: float | None) -> str:
1175
+ if candidate is None or baseline in (None, 0):
1176
+ return "N/A"
1177
+ change = (baseline - candidate) / baseline * 100
1178
+ return f"{abs(change):.1f}% {'less' if change >= 0 else 'more'}"
1179
+
1180
+
1181
+ def _telemetry_by_pair(
1182
+ pairs: Sequence[AlignedPair],
1183
+ candidate_dir: Path,
1184
+ baseline_dir: Path,
1185
+ ) -> list[tuple[AlignedPair, dict[str, Any] | None, dict[str, Any] | None]]:
1186
+ output = []
1187
+ for pair in pairs:
1188
+ sides = []
1189
+ for record, directory in (
1190
+ (pair.candidate, candidate_dir),
1191
+ (pair.baseline, baseline_dir),
1192
+ ):
1193
+ sides.append(
1194
+ recompute_telemetry(
1195
+ record.report,
1196
+ directory,
1197
+ task=record.task,
1198
+ treatment=record.treatment,
1199
+ rep=record.rep,
1200
+ report_path=record.report_path,
1201
+ )
1202
+ )
1203
+ output.append((pair, sides[0], sides[1]))
1204
+ return output
1205
+
1206
+
1207
+ def _render_efficiency_table(
1208
+ telemetry: Sequence[tuple[AlignedPair, dict[str, Any] | None, dict[str, Any] | None]],
1209
+ *,
1210
+ successes_only: bool,
1211
+ ) -> list[str]:
1212
+ selected = [
1213
+ item
1214
+ for item in telemetry
1215
+ if not successes_only
1216
+ or (_run_passed(item[0].candidate.report) and _run_passed(item[0].baseline.report))
1217
+ ]
1218
+ lines = [
1219
+ "| Metric | Complete pairs | Candidate average | Baseline average | Candidate delta |",
1220
+ "|--------|----------------|-------------------|------------------|-----------------|",
1221
+ ]
1222
+ for key, label, kind in _EFFICIENCY_METRICS:
1223
+ values = []
1224
+ for _pair, candidate, baseline in selected:
1225
+ if candidate is None or baseline is None:
1226
+ continue
1227
+ candidate_value = _telemetry_value(candidate, key, kind)
1228
+ baseline_value = _telemetry_value(baseline, key, kind)
1229
+ if candidate_value is not None and baseline_value is not None:
1230
+ values.append((candidate_value, baseline_value))
1231
+ candidate_average = statistics.fmean(value[0] for value in values) if values else None
1232
+ baseline_average = statistics.fmean(value[1] for value in values) if values else None
1233
+ lines.append(
1234
+ f"| {label} | {len(values)}/{len(selected)} | "
1235
+ f"{_format_efficiency(candidate_average, kind)} | "
1236
+ f"{_format_efficiency(baseline_average, kind)} | "
1237
+ f"{_format_reduction(candidate_average, baseline_average)} |"
1238
+ )
1239
+ return lines
1240
+
1241
+
1242
+ def build_aligned_report(
1243
+ candidate_dir: Path,
1244
+ baseline_dir: Path,
1245
+ *,
1246
+ candidate_treatment: str,
1247
+ baseline_treatment: str,
1248
+ tasks_dir: Path,
1249
+ ks: Sequence[int] = (1, 2, 3),
1250
+ ) -> str:
1251
+ """Build a strict two-experiment comparison report."""
1252
+ if not ks or any(isinstance(k, bool) or k < 1 for k in ks):
1253
+ raise ValueError("ks must contain positive integers")
1254
+ normalized_ks = tuple(sorted(set(int(k) for k in ks)))
1255
+ candidate_records = load_treatment_records(
1256
+ candidate_dir,
1257
+ candidate_treatment,
1258
+ tasks_dir=tasks_dir,
1259
+ )
1260
+ baseline_records = load_treatment_records(
1261
+ baseline_dir,
1262
+ baseline_treatment,
1263
+ tasks_dir=tasks_dir,
1264
+ )
1265
+ candidate_matrix_load = load_expected_case_matrix(candidate_dir)
1266
+ baseline_matrix_load = load_expected_case_matrix(baseline_dir)
1267
+ if not candidate_records:
1268
+ raise ValueError(
1269
+ f"no {candidate_treatment} reports found in candidate experiment {candidate_dir}"
1270
+ )
1271
+ if not baseline_records:
1272
+ raise ValueError(
1273
+ f"no {baseline_treatment} reports found in baseline experiment {baseline_dir}"
1274
+ )
1275
+ matrix_issues: list[AlignmentIssue] = []
1276
+ if candidate_matrix_load.error:
1277
+ matrix_issues.append(
1278
+ AlignmentIssue(
1279
+ "*",
1280
+ 0,
1281
+ "invalid-expected-case-matrix",
1282
+ f"candidate: {candidate_matrix_load.error}",
1283
+ )
1284
+ )
1285
+ if baseline_matrix_load.error:
1286
+ matrix_issues.append(
1287
+ AlignmentIssue(
1288
+ "*",
1289
+ 0,
1290
+ "invalid-expected-case-matrix",
1291
+ f"baseline: {baseline_matrix_load.error}",
1292
+ )
1293
+ )
1294
+ candidate_expected = (
1295
+ candidate_matrix_load.matrix.keys_for(candidate_treatment)
1296
+ if candidate_matrix_load.matrix is not None
1297
+ else (frozenset() if candidate_matrix_load.error else None)
1298
+ )
1299
+ baseline_expected = (
1300
+ baseline_matrix_load.matrix.keys_for(baseline_treatment)
1301
+ if baseline_matrix_load.matrix is not None
1302
+ else (frozenset() if baseline_matrix_load.error else None)
1303
+ )
1304
+ if candidate_matrix_load.matrix is not None and not candidate_expected:
1305
+ matrix_issues.append(
1306
+ AlignmentIssue(
1307
+ "*",
1308
+ 0,
1309
+ "expected-treatment-missing",
1310
+ f"candidate matrix has no {candidate_treatment} cases",
1311
+ )
1312
+ )
1313
+ if baseline_matrix_load.matrix is not None and not baseline_expected:
1314
+ matrix_issues.append(
1315
+ AlignmentIssue(
1316
+ "*",
1317
+ 0,
1318
+ "expected-treatment-missing",
1319
+ f"baseline matrix has no {baseline_treatment} cases",
1320
+ )
1321
+ )
1322
+ alignment = align_records(
1323
+ candidate_records,
1324
+ baseline_records,
1325
+ candidate_expected=candidate_expected,
1326
+ baseline_expected=baseline_expected,
1327
+ initial_issues=matrix_issues,
1328
+ )
1329
+ candidate_results = _results_by_task(alignment.pairs, "candidate")
1330
+ baseline_results = _results_by_task(alignment.pairs, "baseline")
1331
+ candidate_metrics = task_macro_pass_metrics(
1332
+ candidate_results,
1333
+ normalized_ks,
1334
+ total_tasks=len(alignment.tasks),
1335
+ )
1336
+ baseline_metrics = task_macro_pass_metrics(
1337
+ baseline_results,
1338
+ normalized_ks,
1339
+ total_tasks=len(alignment.tasks),
1340
+ )
1341
+
1342
+ lines = [
1343
+ "# Comet Aligned Experiment Comparison Report",
1344
+ "",
1345
+ f"- Candidate: `{candidate_treatment}` from `{_experiment_label(candidate_dir)}`",
1346
+ f"- Baseline: `{baseline_treatment}` from `{_experiment_label(baseline_dir)}`",
1347
+ f"- Requested k: {', '.join(str(k) for k in normalized_ks)}",
1348
+ "",
1349
+ "## Alignment contract",
1350
+ "",
1351
+ "- Runs pair only when `task` and repetition match. Two v2 records must also match the full execution-bound `case_hash`; when either side is historical v1, only the shared task-core hash can be checked and the limitation is disclosed below.",
1352
+ "- Expected task/repetition coverage comes from each experiment's controller-written matrix when available, not from the reports that happened to survive.",
1353
+ "- Metrics are computed per task and then macro-averaged; runs are never pooled across tasks.",
1354
+ "- A task with fewer than k aligned repetitions is excluded from that k's task coverage; k is never reduced.",
1355
+ "- Duration is reparsed from raw stdout with the current additive result-event parser. Stored historical duration fields are not mixed into this metric.",
1356
+ "",
1357
+ "## Alignment summary",
1358
+ "",
1359
+ "| Candidate runs | Baseline runs | Expected task/rep keys | Strictly matched pairs | Tasks | Issues |",
1360
+ "|----------------|---------------|------------------------|------------------------|-------|--------|",
1361
+ f"| {len(candidate_records)} | {len(baseline_records)} | {alignment.expected_keys} | {len(alignment.pairs)} | {len(alignment.tasks)} | {len(alignment.issues)} |",
1362
+ "",
1363
+ ]
1364
+
1365
+ matrix_rows = []
1366
+ for side, matrix_load, treatment in (
1367
+ ("Candidate", candidate_matrix_load, candidate_treatment),
1368
+ ("Baseline", baseline_matrix_load, baseline_treatment),
1369
+ ):
1370
+ if matrix_load.matrix is None:
1371
+ status = "invalid (fail closed)" if matrix_load.error else "historical fallback"
1372
+ matrix_rows.append(f"| {side} | {status} | N/A | N/A |")
1373
+ else:
1374
+ keys = matrix_load.matrix.keys_for(treatment)
1375
+ matrix_rows.append(
1376
+ f"| {side} | report-bound | {_hash_short(matrix_load.matrix.matrix_hash)} | {len(keys)} |"
1377
+ )
1378
+ lines.extend(
1379
+ [
1380
+ "## Expected case matrix audit",
1381
+ "",
1382
+ "| Experiment | Source | Matrix hash | Target cases |",
1383
+ "|------------|--------|-------------|--------------|",
1384
+ *matrix_rows,
1385
+ "",
1386
+ ]
1387
+ )
1388
+ missing_matrices = [
1389
+ side
1390
+ for side, matrix_load in (
1391
+ ("candidate", candidate_matrix_load),
1392
+ ("baseline", baseline_matrix_load),
1393
+ )
1394
+ if matrix_load.matrix is None and matrix_load.error is None
1395
+ ]
1396
+ if missing_matrices:
1397
+ lines.extend(
1398
+ [
1399
+ "> Expected-matrix limitation: "
1400
+ + " and ".join(missing_matrices)
1401
+ + " experiment(s) predate a valid controller-written matrix. Observed-report fallback is used for that side, so a case missing from every fallback source cannot be reconstructed or claimed as historically executed.",
1402
+ "",
1403
+ ]
1404
+ )
1405
+ invalid_matrices = [
1406
+ side
1407
+ for side, matrix_load in (
1408
+ ("candidate", candidate_matrix_load),
1409
+ ("baseline", baseline_matrix_load),
1410
+ )
1411
+ if matrix_load.error is not None
1412
+ ]
1413
+ if invalid_matrices:
1414
+ lines.extend(
1415
+ [
1416
+ "> Invalid-matrix handling: "
1417
+ + " and ".join(invalid_matrices)
1418
+ + " experiment matrix failed validation. Its observed reports are excluded rather than treated as a historical fallback.",
1419
+ "",
1420
+ ]
1421
+ )
1422
+
1423
+ fallback_count = sum(
1424
+ 1 for record in (*candidate_records, *baseline_records) if not record.manifest.run_bound
1425
+ )
1426
+ if fallback_count:
1427
+ lines.extend(
1428
+ [
1429
+ f"> Audit limitation: {fallback_count} run record(s) predate report-bound case manifests. Their hashes were reconstructed from the current canonical task tree and are marked `current-task-tree-fallback`; they prove comparison-time parity, not the exact historical checkout.",
1430
+ "",
1431
+ ]
1432
+ )
1433
+ execution_unbound_count = sum(
1434
+ 1
1435
+ for record in (*candidate_records, *baseline_records)
1436
+ if not record.manifest.execution_bound
1437
+ )
1438
+ if execution_unbound_count:
1439
+ lines.extend(
1440
+ [
1441
+ f"> Execution-identity limitation: {execution_unbound_count} run record(s) use v1 or reconstructed manifests that do not bind the runner/controller sources, immutable Docker image, Claude tool version, model selection, and interaction configuration. They must not be presented as exact historical execution identity.",
1442
+ "",
1443
+ ]
1444
+ )
1445
+
1446
+ lines.extend(
1447
+ [
1448
+ "## pass@k / pass^k — task macro average",
1449
+ "",
1450
+ "Each value is the mean of the per-task estimator. Coverage is shown as eligible tasks / all aligned task names.",
1451
+ "`pass@k` uses the HumanEval at-least-one-success estimator. `pass^k` keeps Comet's observed consistency lower bound: a task contributes 1 only when all of its aligned repetitions pass.",
1452
+ "",
1453
+ "| Treatment | Metric | "
1454
+ + " | ".join(f"k={k}" for k in normalized_ks)
1455
+ + " | Matched pass/fail |",
1456
+ "|-----------|--------|"
1457
+ + "|".join("-----" for _ in normalized_ks)
1458
+ + "|-------------------|",
1459
+ ]
1460
+ )
1461
+ for treatment, results, metrics in (
1462
+ (candidate_treatment, candidate_results, candidate_metrics),
1463
+ (baseline_treatment, baseline_results, baseline_metrics),
1464
+ ):
1465
+ total_passes = sum(sum(1 for value in values if value) for values in results.values())
1466
+ total_runs = sum(len(values) for values in results.values())
1467
+ pass_at_cells = [
1468
+ f"{_format_metric(metrics[k]['pass_at_k'])} "
1469
+ f"({metrics[k]['eligible_tasks']}/{metrics[k]['total_tasks']} tasks)"
1470
+ for k in normalized_ks
1471
+ ]
1472
+ pass_pow_cells = [
1473
+ f"{_format_metric(metrics[k]['pass_pow_k'])} "
1474
+ f"({metrics[k]['eligible_tasks']}/{metrics[k]['total_tasks']} tasks)"
1475
+ for k in normalized_ks
1476
+ ]
1477
+ lines.append(
1478
+ f"| {treatment} | pass@k | "
1479
+ + " | ".join(pass_at_cells)
1480
+ + f" | {total_passes}/{total_runs} |"
1481
+ )
1482
+ lines.append(
1483
+ f"| {treatment} | pass^k | "
1484
+ + " | ".join(pass_pow_cells)
1485
+ + f" | {total_passes}/{total_runs} |"
1486
+ )
1487
+ lines.append("")
1488
+
1489
+ lines.extend(
1490
+ [
1491
+ "## Duration from raw stdout",
1492
+ "",
1493
+ "Missing raw duration is reported as missing coverage and is not replaced by, or averaged with, the stored report duration.",
1494
+ "",
1495
+ "| Treatment | Matched runs | Duration coverage | Total | Average / covered run | Source |",
1496
+ "|-----------|--------------|-------------------|-------|-----------------------|--------|",
1497
+ ]
1498
+ )
1499
+ duration_missing: list[tuple[str, tuple[tuple[str, int], ...]]] = []
1500
+ for treatment, side, directory in (
1501
+ (candidate_treatment, "candidate", candidate_dir),
1502
+ (baseline_treatment, "baseline", baseline_dir),
1503
+ ):
1504
+ matched, covered, total, average, missing = _duration_summary(
1505
+ alignment.pairs,
1506
+ side,
1507
+ directory,
1508
+ )
1509
+ lines.append(
1510
+ f"| {treatment} | {matched} | {covered}/{matched} | "
1511
+ f"{_format_seconds(total)} | {_format_seconds(average)} | {DURATION_SOURCE} |"
1512
+ )
1513
+ if missing:
1514
+ duration_missing.append((treatment, missing))
1515
+ lines.append("")
1516
+ for treatment, missing in duration_missing:
1517
+ references = ", ".join(f"`{task}#r{rep}`" for task, rep in missing)
1518
+ lines.append(f"- {treatment} missing raw duration: {references}")
1519
+ if duration_missing:
1520
+ lines.append("")
1521
+
1522
+ paired_telemetry = _telemetry_by_pair(alignment.pairs, candidate_dir, baseline_dir)
1523
+ successful_pairs = sum(
1524
+ _run_passed(pair.candidate.report) and _run_passed(pair.baseline.report)
1525
+ for pair in alignment.pairs
1526
+ )
1527
+ lines.extend(
1528
+ [
1529
+ "## Paired task efficiency from raw stdout",
1530
+ "",
1531
+ "Result telemetry is additive across the initial model start, deterministic user-answer resumes, and cold resumes. Token totals include cache-read tokens. Context input deduplicates streamed assistant events by message id.",
1532
+ "",
1533
+ f"### Strict-success intersection ({successful_pairs} paired runs)",
1534
+ "",
1535
+ "This is the primary completed-task efficiency view: both candidate and baseline passed the same task repetition.",
1536
+ "",
1537
+ *_render_efficiency_table(paired_telemetry, successes_only=True),
1538
+ "",
1539
+ f"### All aligned runs ({len(alignment.pairs)} paired runs)",
1540
+ "",
1541
+ *_render_efficiency_table(paired_telemetry, successes_only=False),
1542
+ "",
1543
+ "> Context rows require per-assistant-message usage from both sides. Low pair coverage means the historical CLI did not preserve enough context telemetry; those rows must not be promoted as a workflow comparison.",
1544
+ "",
1545
+ ]
1546
+ )
1547
+
1548
+ candidate_manifests = _manifest_by_task(candidate_records)
1549
+ baseline_manifests = _manifest_by_task(baseline_records)
1550
+ lines.extend(
1551
+ [
1552
+ "## Case manifest audit",
1553
+ "",
1554
+ "Hash cells show the first 12 hexadecimal characters. Full hashes remain in the source report when report-bound; fallback hashes are reproducible from the task tree.",
1555
+ "",
1556
+ "| Task | Candidate case | Baseline case | Source | Instruction | Validator | Environment | Prompt | Runner | Controller | Execution |",
1557
+ "|------|----------------|---------------|--------|-------------|-----------|-------------|--------|--------|------------|-----------|",
1558
+ ]
1559
+ )
1560
+ for task in alignment.tasks:
1561
+ candidate_manifest = candidate_manifests.get(task)
1562
+ baseline_manifest = baseline_manifests.get(task)
1563
+ representative = candidate_manifest or baseline_manifest
1564
+ if representative is None:
1565
+ continue
1566
+ source = "/".join(
1567
+ filter(
1568
+ None,
1569
+ (
1570
+ candidate_manifest.source if candidate_manifest else "candidate-missing",
1571
+ baseline_manifest.source if baseline_manifest else "baseline-missing",
1572
+ ),
1573
+ )
1574
+ )
1575
+ lines.append(
1576
+ f"| {task} | "
1577
+ f"{_hash_short(candidate_manifest.case_hash) if candidate_manifest else '—'} | "
1578
+ f"{_hash_short(baseline_manifest.case_hash) if baseline_manifest else '—'} | "
1579
+ f"{source} | {_hash_short(representative.instruction_hash)} | "
1580
+ f"{_hash_short(representative.validator_hash)} | "
1581
+ f"{_hash_short(representative.environment_hash)} | "
1582
+ f"{_hash_short(representative.prompt_hash)} | "
1583
+ f"{_hash_short(representative.runner_hash) if representative.runner_hash else 'v1-unbound'} | "
1584
+ f"{_hash_short(representative.controller_hash) if representative.controller_hash else 'v1-unbound'} | "
1585
+ f"{_hash_short(representative.execution_hash) if representative.execution_hash else 'v1-unbound'} |"
1586
+ )
1587
+ lines.append("")
1588
+
1589
+ lines.extend(
1590
+ [
1591
+ "## Task outcomes on strictly matched repetitions",
1592
+ "",
1593
+ "| Task | Repetitions | Candidate | Baseline |",
1594
+ "|------|-------------|-----------|----------|",
1595
+ ]
1596
+ )
1597
+ pairs_by_task: dict[str, list[AlignedPair]] = defaultdict(list)
1598
+ for pair in alignment.pairs:
1599
+ pairs_by_task[pair.task].append(pair)
1600
+ for task in alignment.tasks:
1601
+ pairs = sorted(pairs_by_task.get(task, []), key=lambda item: item.rep)
1602
+ reps = ",".join(str(pair.rep) for pair in pairs) or "—"
1603
+ candidate_passes = sum(_run_passed(pair.candidate.report) for pair in pairs)
1604
+ baseline_passes = sum(_run_passed(pair.baseline.report) for pair in pairs)
1605
+ lines.append(
1606
+ f"| {task} | {reps} | {candidate_passes}/{len(pairs)} | "
1607
+ f"{baseline_passes}/{len(pairs)} |"
1608
+ )
1609
+ lines.append("")
1610
+
1611
+ if alignment.issues:
1612
+ counts = Counter(issue.reason for issue in alignment.issues)
1613
+ lines.extend(
1614
+ [
1615
+ "## Alignment issues",
1616
+ "",
1617
+ "Issue counts: "
1618
+ + ", ".join(f"`{reason}`={count}" for reason, count in sorted(counts.items())),
1619
+ "",
1620
+ "| Task | Rep | Reason | Detail |",
1621
+ "|------|-----|--------|--------|",
1622
+ ]
1623
+ )
1624
+ for issue in alignment.issues:
1625
+ detail = issue.detail.replace("|", "/")
1626
+ lines.append(f"| {issue.task} | {issue.rep} | {issue.reason} | {detail} |")
1627
+ lines.append("")
1628
+ else:
1629
+ lines.extend(["## Alignment issues", "", "_None._", ""])
1630
+
1631
+ return "\n".join(lines)