@cassiomc1/forgeloop 1.13.0 → 2.0.0-rc.2

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 (395) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/CONTRIBUTING.md +11 -7
  3. package/DOCS_INDEX.md +37 -4
  4. package/ENG/c-development-eng.md +2 -2
  5. package/ENG/cpp-development-eng.md +2 -2
  6. package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
  7. package/ENG/flutter-development-eng.md +4 -2
  8. package/ENG/go-development-eng.md +2 -2
  9. package/ENG/java-development-eng.md +3 -3
  10. package/ENG/nodejs-backend-development-eng.md +10 -6
  11. package/ENG/php-development-eng.md +3 -3
  12. package/ENG/python-development-eng.md +205 -0
  13. package/ENG/rust-development-eng.md +42 -1
  14. package/ENG/sec-code-eng.md +7 -7
  15. package/ENG/sql-development-eng.md +4 -4
  16. package/ENG/swift-development-eng.md +5 -4
  17. package/ENG/test-code-eng.md +1 -1
  18. package/ENG/typescript-development-eng.md +4 -4
  19. package/EXECUTION_STATE.md +12 -0
  20. package/GUIDE_ROUTER.md +6 -0
  21. package/LICENSE +133 -21
  22. package/LOOP_ENGINEERING.md +28 -2
  23. package/LOOP_SYSTEM_DESIGN.md +10 -10
  24. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  25. package/PROTOCOL_INTEGRATION.md +55 -2
  26. package/README.md +63 -59
  27. package/TERMINOLOGY.md +2 -0
  28. package/THIRD_PARTY_NOTICES.md +3 -4
  29. package/THREAT_MODEL.md +140 -1
  30. package/completions/_forgeloop +29 -3
  31. package/completions/forgeloop.bash +55 -3
  32. package/completions/forgeloop.fish +183 -1
  33. package/docs/ADVISORY_CONTEXT.md +25 -0
  34. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  35. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  36. package/docs/AGENT_PROTOCOL_SUMMARY.md +74 -39
  37. package/docs/AGENT_SKILL.md +66 -0
  38. package/docs/ARTIFACT_REFERENCE.md +171 -35
  39. package/docs/AUDIT_UX.md +46 -0
  40. package/docs/BROWSER_VERIFICATION.md +136 -0
  41. package/docs/CLI_REFERENCE.md +512 -8
  42. package/docs/CODE_ATTESTATION.md +2 -2
  43. package/docs/DOCUMENTATION_GUIDE.md +34 -11
  44. package/docs/GETTING_STARTED.md +3 -3
  45. package/docs/JEV_BENCHMARKS.md +31 -0
  46. package/docs/MODEL_ROUTING.md +37 -0
  47. package/docs/OPENSRC_ADAPTER.md +241 -0
  48. package/docs/PACKAGE_CONTENTS.md +46 -18
  49. package/docs/PROVIDERS.md +126 -0
  50. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  51. package/docs/RECIPES.md +9 -0
  52. package/docs/RELEASE_CHECKLIST.md +116 -12
  53. package/docs/SECURITY_REVIEW.md +71 -0
  54. package/docs/SELF_HOSTED_TESTS.md +121 -0
  55. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  56. package/docs/TEST_INTELLIGENCE.md +29 -0
  57. package/docs/TEST_PRUNING.md +14 -0
  58. package/docs/TROUBLESHOOTING.md +198 -1
  59. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  60. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  61. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  62. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  63. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  64. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  65. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  66. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  67. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  68. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  69. package/docs/diagrams/README.md +13 -9
  70. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  71. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  72. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  73. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  74. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  75. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  76. package/docs/documentation-manifest.json +774 -16
  77. package/docs/protocol-requirements.json +24 -0
  78. package/package.json +43 -11
  79. package/schemas/config.schema.json +14 -0
  80. package/schemas/context-plan.schema.json +18 -0
  81. package/schemas/semantic-decision.schema.json +46 -0
  82. package/schemas/task-bundle.schema.json +7 -2
  83. package/schemas/test-utility.schema.json +44 -0
  84. package/scripts/CI_VALIDATORS.md +6 -6
  85. package/scripts/benchmark-jev.mjs +5 -0
  86. package/scripts/benchmark-test-intelligence.mjs +4 -0
  87. package/scripts/generate-agent-protocol-summary.mjs +6 -3
  88. package/scripts/generate-forgeloop-skill.mjs +133 -0
  89. package/scripts/jev-smoke.mjs +19 -0
  90. package/scripts/run-mcp-clean-tests.mjs +30 -0
  91. package/skills/forgeloop/README.md +9 -0
  92. package/skills/forgeloop/SKILL.md +77 -0
  93. package/skills/forgeloop/references/lifecycle.md +9 -0
  94. package/skills/forgeloop/references/recovery.md +7 -0
  95. package/skills/forgeloop/references/verification.md +7 -0
  96. package/src/adapters/agent-browser/assertions.js +47 -0
  97. package/src/adapters/agent-browser/commands.js +54 -0
  98. package/src/adapters/agent-browser/index.js +3 -0
  99. package/src/adapters/agent-browser/locator.js +40 -0
  100. package/src/adapters/agent-browser/process.js +215 -0
  101. package/src/adapters/agent-browser/provider.js +316 -0
  102. package/src/adapters/emulated-services/constants.js +24 -0
  103. package/src/adapters/emulated-services/index.js +7 -0
  104. package/src/adapters/emulated-services/process.js +162 -0
  105. package/src/adapters/emulated-services/provider.js +289 -0
  106. package/src/adapters/opensrc/normalize.js +90 -0
  107. package/src/adapters/opensrc/process.js +248 -0
  108. package/src/adapters/opensrc/provider.js +338 -0
  109. package/src/adapters/opensrc/search.js +264 -0
  110. package/src/adapters/temporary-paths.js +18 -0
  111. package/src/adapters/typesafe/client.js +28 -0
  112. package/src/adapters/typesafe/engine.js +63 -0
  113. package/src/adapters/typesafe/normalize.js +41 -0
  114. package/src/cli.js +218 -174
  115. package/src/commands/action-show.js +5 -2
  116. package/src/commands/advance.js +5 -1
  117. package/src/commands/baseline.js +13 -11
  118. package/src/commands/checkpoint-revalidate.js +192 -0
  119. package/src/commands/complete.js +19 -3
  120. package/src/commands/context-plan.js +38 -0
  121. package/src/commands/contract-create.js +266 -0
  122. package/src/commands/contract-revise.js +236 -0
  123. package/src/commands/decision-show.js +14 -0
  124. package/src/commands/decision-status.js +22 -0
  125. package/src/commands/discover.js +41 -0
  126. package/src/commands/doctor.js +51 -13
  127. package/src/commands/gate-record.js +194 -0
  128. package/src/commands/gate-revalidate.js +135 -0
  129. package/src/commands/handoff-list.js +6 -1
  130. package/src/commands/handoff-show.js +6 -1
  131. package/src/commands/migrate-protocol.js +2 -2
  132. package/src/commands/model-route.js +40 -0
  133. package/src/commands/next.js +7 -1
  134. package/src/commands/policy-diff.js +25 -4
  135. package/src/commands/progress.js +17 -11
  136. package/src/commands/record-diagnosis.js +3 -0
  137. package/src/commands/route.js +146 -18
  138. package/src/commands/semantic-plan.js +41 -0
  139. package/src/commands/status.js +6 -1
  140. package/src/commands/storage-backup.js +15 -0
  141. package/src/commands/storage-migrate.js +5 -0
  142. package/src/commands/storage-migration-resume.js +5 -0
  143. package/src/commands/storage-migration-status.js +44 -0
  144. package/src/commands/storage-restore-resume.js +7 -0
  145. package/src/commands/storage-restore.js +8 -0
  146. package/src/commands/task-abandon.js +224 -0
  147. package/src/commands/task-list.js +8 -43
  148. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  149. package/src/commands/task-migrate.js +3 -3
  150. package/src/commands/task-recover.js +10 -2
  151. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  152. package/src/commands/task-scope.js +18 -2
  153. package/src/commands/task-show.js +16 -2
  154. package/src/commands/test-inventory.js +5 -0
  155. package/src/commands/test-prune-plan.js +5 -0
  156. package/src/commands/test-prune-probe.js +5 -0
  157. package/src/commands/test-utility.js +5 -0
  158. package/src/commands/validate-protocol.js +39 -15
  159. package/src/commands/validate-receipt.js +8 -1
  160. package/src/config/guides.json +4 -0
  161. package/src/core/action-ledger-projection.js +10 -3
  162. package/src/core/actions.js +38 -75
  163. package/src/core/activation.js +11 -3
  164. package/src/core/approvals.js +17 -51
  165. package/src/core/artifact-registry.js +83 -2
  166. package/src/core/artifacts.js +81 -27
  167. package/src/core/attachment-paths.js +45 -0
  168. package/src/core/attestation-coverage.js +6 -1
  169. package/src/core/attestation-verifier.js +21 -2
  170. package/src/core/attestation.js +17 -6
  171. package/src/core/audit-ux.js +514 -0
  172. package/src/core/audit.js +6 -1
  173. package/src/core/browser-verification/constants.js +149 -0
  174. package/src/core/browser-verification/normalize.js +254 -0
  175. package/src/core/browser-verification/provider.js +519 -0
  176. package/src/core/browser-verification/service.js +115 -0
  177. package/src/core/bundles.js +160 -66
  178. package/src/core/checkpoint-revalidation.js +321 -0
  179. package/src/core/cli-command-definitions.js +350 -7
  180. package/src/core/code-manifest.js +9 -3
  181. package/src/core/command-executors.js +274 -108
  182. package/src/core/command-input.js +136 -44
  183. package/src/core/completion-artifacts.js +85 -34
  184. package/src/core/completion-ownership.js +19 -8
  185. package/src/core/completion-recovery-rebind.js +11 -1
  186. package/src/core/completion.js +64 -15
  187. package/src/core/config.js +3 -0
  188. package/src/core/conformance.js +2 -1
  189. package/src/core/context-compiler/budget.js +9 -0
  190. package/src/core/context-compiler/candidates.js +39 -0
  191. package/src/core/context-compiler/compiler.js +63 -0
  192. package/src/core/context-compiler/fingerprint.js +11 -0
  193. package/src/core/context-compiler/policy.js +13 -0
  194. package/src/core/context-compiler/result.js +23 -0
  195. package/src/core/continuity-lint.js +13 -2
  196. package/src/core/continuity-reconciliation.js +16 -3
  197. package/src/core/continuity.js +16 -8
  198. package/src/core/contract-bootstrap-recovery.js +656 -0
  199. package/src/core/contract-revision.js +209 -0
  200. package/src/core/decision/artifact.js +70 -0
  201. package/src/core/decision/benchmarks.js +103 -0
  202. package/src/core/decision/cache.js +27 -0
  203. package/src/core/decision/constants.js +58 -0
  204. package/src/core/decision/cutover.js +34 -0
  205. package/src/core/decision/engine.js +22 -0
  206. package/src/core/decision/errors.js +68 -0
  207. package/src/core/decision/events.js +104 -0
  208. package/src/core/decision/freshness.js +19 -0
  209. package/src/core/decision/normalizers/index.js +115 -0
  210. package/src/core/decision/policy.js +18 -0
  211. package/src/core/decision/projection.js +16 -0
  212. package/src/core/decision/question-registry.js +201 -0
  213. package/src/core/decision/request.js +26 -0
  214. package/src/core/decision/resolver.js +138 -0
  215. package/src/core/decision/result.js +58 -0
  216. package/src/core/decision/service.js +181 -0
  217. package/src/core/decision/state-builder.js +65 -0
  218. package/src/core/decision/task-bindings.js +35 -0
  219. package/src/core/decision/test-provider.js +32 -0
  220. package/src/core/decision/thresholds.js +15 -0
  221. package/src/core/diagnosis-model.js +11 -0
  222. package/src/core/diagnosis.js +10 -1
  223. package/src/core/diagnostic-projection.js +29 -12
  224. package/src/core/diagnostic-record.js +28 -3
  225. package/src/core/efficiency.js +6 -1
  226. package/src/core/error-codes.js +278 -0
  227. package/src/core/events.js +535 -147
  228. package/src/core/evidence-readiness.js +9 -0
  229. package/src/core/execution-prerequisites.js +36 -13
  230. package/src/core/execution-profile.js +63 -38
  231. package/src/core/execution.js +26 -40
  232. package/src/core/filesystem.js +11 -0
  233. package/src/core/gate-artifact.js +105 -19
  234. package/src/core/gate-provenance.js +125 -0
  235. package/src/core/handoff.js +26 -14
  236. package/src/core/history.js +39 -35
  237. package/src/core/hypothesis-projection.js +32 -0
  238. package/src/core/information-gain-projection.js +104 -100
  239. package/src/core/inspect.js +12 -3
  240. package/src/core/integration-invocation-policy.js +37 -4
  241. package/src/core/integration-resources.js +119 -87
  242. package/src/core/ledger-event-collection.js +189 -0
  243. package/src/core/ledger-relations.js +23 -0
  244. package/src/core/model-router/constants.js +10 -0
  245. package/src/core/model-router/policy.js +103 -0
  246. package/src/core/model-router/router.js +37 -0
  247. package/src/core/native-storage.js +12 -0
  248. package/src/core/next-action-continuity.js +31 -9
  249. package/src/core/next-action-model.js +58 -0
  250. package/src/core/next-action-phases.js +131 -43
  251. package/src/core/next-action-refresh.js +43 -9
  252. package/src/core/next-action-review-phase.js +9 -4
  253. package/src/core/next-action-verification-phase.js +2 -1
  254. package/src/core/next-action.js +56 -9
  255. package/src/core/phase.js +266 -103
  256. package/src/core/policy-engine.js +12 -13
  257. package/src/core/preflight-consistency.js +89 -62
  258. package/src/core/preflight-loaders.js +48 -9
  259. package/src/core/preflight-model.js +6 -1
  260. package/src/core/preflight.js +29 -12
  261. package/src/core/progress.js +21 -10
  262. package/src/core/protocol-info.js +68 -0
  263. package/src/core/protocol-migration.js +7 -6
  264. package/src/core/protocol.js +20 -0
  265. package/src/core/reconcile-closure.js +281 -67
  266. package/src/core/recovery-history.js +67 -47
  267. package/src/core/reflection.js +72 -77
  268. package/src/core/responsibility.js +25 -13
  269. package/src/core/resumability.js +154 -44
  270. package/src/core/route-artifact.js +28 -4
  271. package/src/core/router.js +67 -1
  272. package/src/core/runtime-context.js +123 -61
  273. package/src/core/schema-validation.js +11 -3
  274. package/src/core/security-review/constants.js +64 -0
  275. package/src/core/security-review/normalize.js +245 -0
  276. package/src/core/security-review/provider.js +204 -0
  277. package/src/core/security-review/service.js +134 -0
  278. package/src/core/semantic-planning/constants.js +19 -0
  279. package/src/core/semantic-planning/projection.js +94 -0
  280. package/src/core/semantic-planning/service.js +15 -0
  281. package/src/core/settlement.js +12 -1
  282. package/src/core/signing/bundle-file.js +53 -0
  283. package/src/core/signing/bundle-reference.js +15 -0
  284. package/src/core/signing/sigstore.js +60 -33
  285. package/src/core/signing/statement-file.js +27 -0
  286. package/src/core/sources.js +37 -0
  287. package/src/core/storage-runtime-registry.js +41 -0
  288. package/src/core/structural-quality/artifacts.js +20 -14
  289. package/src/core/structural-quality/service.js +81 -56
  290. package/src/core/target-layout.js +1 -1
  291. package/src/core/task-claim-state.js +249 -15
  292. package/src/core/task-command.js +21 -1
  293. package/src/core/task-conflict-inspection.js +133 -102
  294. package/src/core/task-descriptor.js +29 -22
  295. package/src/core/task-discovery.js +288 -90
  296. package/src/core/task-lock.js +66 -306
  297. package/src/core/task-migration-validation.js +5 -5
  298. package/src/core/task-migration.js +17 -287
  299. package/src/core/task-paths.js +13 -0
  300. package/src/core/task-recovery.js +1 -0
  301. package/src/core/task-snapshot.js +51 -8
  302. package/src/core/task-storage-migration.js +18 -0
  303. package/src/core/templates.js +23 -8
  304. package/src/core/test-intelligence/benchmarks.js +68 -0
  305. package/src/core/test-intelligence/inventory.js +73 -0
  306. package/src/core/test-intelligence/prune.js +94 -0
  307. package/src/core/test-intelligence/semantic-state.js +15 -0
  308. package/src/core/test-intelligence/service.js +40 -0
  309. package/src/core/test-intelligence/utility.js +50 -0
  310. package/src/core/trace.js +164 -105
  311. package/src/core/trajectory-evaluation.js +15 -3
  312. package/src/core/trajectory-metrics.js +47 -32
  313. package/src/core/transaction.js +16 -287
  314. package/src/core/verification-scope.js +20 -12
  315. package/src/core/work-state.js +100 -59
  316. package/src/integration.d.ts +492 -0
  317. package/src/integration.js +54 -0
  318. package/src/providers/README.md +47 -0
  319. package/src/providers/capabilities.js +46 -0
  320. package/src/providers/errors.js +15 -0
  321. package/src/providers/index.js +29 -0
  322. package/src/providers/json-snapshot.js +105 -0
  323. package/src/providers/registry.js +152 -0
  324. package/src/repository-index/binary-manager.js +7 -5
  325. package/src/storage/artifact-bytes.js +22 -0
  326. package/src/storage/attachment-binding.js +11 -0
  327. package/src/storage/attachment-files.js +82 -0
  328. package/src/storage/attachment-references.js +36 -0
  329. package/src/storage/backup.js +53 -0
  330. package/src/storage/capabilities.js +31 -0
  331. package/src/storage/connection-owner.js +119 -0
  332. package/src/storage/connection.js +376 -0
  333. package/src/storage/existing-project-scope.js +45 -0
  334. package/src/storage/export-destination.js +44 -0
  335. package/src/storage/exporter.js +284 -0
  336. package/src/storage/file-durability.js +46 -0
  337. package/src/storage/importer.js +461 -0
  338. package/src/storage/index.js +74 -0
  339. package/src/storage/leases.js +63 -0
  340. package/src/storage/ledger-event-snapshot.js +150 -0
  341. package/src/storage/ledger-relations.js +220 -0
  342. package/src/storage/legacy-maintenance-boundary.js +18 -0
  343. package/src/storage/legacy-signature.js +115 -0
  344. package/src/storage/maintenance-handoff.js +129 -0
  345. package/src/storage/maintenance-owner.js +42 -0
  346. package/src/storage/maintenance.js +310 -0
  347. package/src/storage/metadata-json.js +35 -0
  348. package/src/storage/migration-activate.js +59 -0
  349. package/src/storage/migration-archive.js +57 -0
  350. package/src/storage/migration-attachment-root.js +61 -0
  351. package/src/storage/migration-candidate.js +398 -0
  352. package/src/storage/migration-publication.js +143 -0
  353. package/src/storage/migration-rollback-admission.js +54 -0
  354. package/src/storage/migration-rollback-preparation.js +132 -0
  355. package/src/storage/migration-rollback-publication.js +183 -0
  356. package/src/storage/migration-rollback-release.js +66 -0
  357. package/src/storage/migration-rollback-source-stage.js +103 -0
  358. package/src/storage/migration-rollback.js +43 -0
  359. package/src/storage/migration-source-partition.js +56 -0
  360. package/src/storage/migration-source.js +255 -0
  361. package/src/storage/migration-terminal.js +39 -0
  362. package/src/storage/migration-validation.js +75 -0
  363. package/src/storage/migration.js +43 -0
  364. package/src/storage/operational-attachments.js +49 -0
  365. package/src/storage/operational-context.js +50 -0
  366. package/src/storage/project-backup.js +98 -0
  367. package/src/storage/project-bootstrap.js +24 -0
  368. package/src/storage/project-boundary.js +79 -0
  369. package/src/storage/project-read-snapshot.js +18 -0
  370. package/src/storage/project-replacement.js +137 -0
  371. package/src/storage/project-restore.js +203 -0
  372. package/src/storage/record-definitions.js +45 -0
  373. package/src/storage/repository.js +507 -0
  374. package/src/storage/restore-journal.js +62 -0
  375. package/src/storage/restore-layout.js +30 -0
  376. package/src/storage/restore-preparation.js +84 -0
  377. package/src/storage/restore-replacement-archive.js +162 -0
  378. package/src/storage/restore-replacement-owner.js +31 -0
  379. package/src/storage/restore-replacement-preparation.js +128 -0
  380. package/src/storage/restore-replacement.js +142 -0
  381. package/src/storage/restore-terminal.js +32 -0
  382. package/src/storage/runtime.js +31 -0
  383. package/src/storage/schema.js +214 -0
  384. package/src/storage/singleton-import.js +84 -0
  385. package/src/storage/snapshot-continuity.js +16 -0
  386. package/src/storage/snapshot.js +36 -0
  387. package/src/storage/storage-marker.js +79 -0
  388. package/src/storage/task-guards.js +107 -0
  389. package/src/storage/topology.js +42 -0
  390. package/src/storage/transaction.js +179 -0
  391. package/src/storage/unit-of-work.js +663 -0
  392. package/src/storage/windows-process-incarnation.js +206 -0
  393. package/.forgeloop/forgeloop.gitignore +0 -4
  394. package/LICENSE-DOCS.md +0 -13
  395. package/src/core/transaction-maintenance.js +0 -70
@@ -0,0 +1,73 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const TEST_DIRECTORIES = Object.freeze(["test", "tests", "spec", "specs"]);
6
+ const TEST_FILE_PATTERN = /\.(?:test|spec)\.(?:c|cpp|go|java|js|jsx|mjs|py|rb|rs|ts|tsx)$/i;
7
+
8
+ async function filesUnder(root, relative = "") {
9
+ const current = path.join(root, relative);
10
+ let entries = [];
11
+ try { entries = await readdir(current, { withFileTypes: true }); } catch { return []; }
12
+ const result = [];
13
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
14
+ const child = path.join(relative, entry.name);
15
+ if (entry.isDirectory()) result.push(...await filesUnder(root, child));
16
+ else if (entry.isFile() && TEST_FILE_PATTERN.test(entry.name)) result.push(child);
17
+ }
18
+ return result;
19
+ }
20
+
21
+ function stableId(framework, file, suite, name) {
22
+ return `test-${createHash("sha256").update(JSON.stringify({ framework, file, suite, name })).digest("hex").slice(0, 24)}`;
23
+ }
24
+
25
+ function frameworkFor(source) {
26
+ if (/node:test/.test(source)) return "node:test";
27
+ if (/vitest/.test(source)) return "vitest";
28
+ if (/jest/.test(source)) return "jest";
29
+ if (/pytest|unittest/.test(source)) return "python";
30
+ return "unknown";
31
+ }
32
+
33
+ function collectUnits(source) {
34
+ const units = [];
35
+ const matcher = /\b(test|it)\s*\(\s*(["'`])([^"'`]+)\2/g;
36
+ let match;
37
+ while ((match = matcher.exec(source))) units.push({ name: match[3].trim(), line: source.slice(0, match.index).split("\n").length });
38
+ return units;
39
+ }
40
+
41
+ function signals(file, name) {
42
+ const text = `${file} ${name}`.toLowerCase();
43
+ return {
44
+ requiredBehavior: /contract|acceptance|required|lifecycle|protocol/.test(text),
45
+ security: /security|auth|secret|permission|credential|csp|injection/.test(text),
46
+ publicApi: /api|compat|public|cli|protocol/.test(text),
47
+ criticalPath: /critical|transaction|lock|concurr|race|recovery/.test(text),
48
+ integration: /integration|e2e|browser|provider|package|release/.test(text),
49
+ migration: /migration|schema|upgrade|downgrade/.test(text),
50
+ release: /package|release|publish|smoke/.test(text),
51
+ platformSpecific: /windows|win32|macos|darwin|linux|platform|native/.test(text),
52
+ };
53
+ }
54
+
55
+ export async function inventoryTests(projectRoot = process.cwd()) {
56
+ const files = [];
57
+ for (const directory of TEST_DIRECTORIES) files.push(...await filesUnder(projectRoot, directory));
58
+ const tests = [];
59
+ for (const relativeFile of [...new Set(files)].sort()) {
60
+ const source = await readFile(path.join(projectRoot, relativeFile), "utf8");
61
+ const framework = frameworkFor(source);
62
+ for (const unit of collectUnits(source)) {
63
+ const file = relativeFile.replaceAll(path.sep, "/");
64
+ tests.push({
65
+ testId: stableId(framework, file, "", unit.name), file, framework, suite: "", name: unit.name,
66
+ line: unit.line, sourceSummary: `${file}:${unit.line} ${unit.name}`.slice(0, 500),
67
+ targets: [file], runtimeMs: null, uniqueBranches: null, signals: signals(file, unit.name),
68
+ });
69
+ }
70
+ }
71
+ tests.sort((a, b) => a.testId.localeCompare(b.testId));
72
+ return { schemaVersion: 1, source: "DETERMINISTIC", tests };
73
+ }
@@ -0,0 +1,94 @@
1
+ import { readTestUtility } from "./service.js";
2
+ import { assertSafePath } from "../filesystem.js";
3
+ import { cp, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const PROTECTED_CLASSES = new Set([
11
+ "KEEP_REQUIRED", "KEEP_RISK_GUARD", "KEEP_UNIQUE", "KEEP_AUTHORITY_BOUNDARY",
12
+ "KEEP_RECOVERY_INVARIANT", "KEEP_RELEASE_SMOKE", "KEEP_MIGRATION_COMPATIBILITY",
13
+ "KEEP_PLATFORM_BEHAVIOR",
14
+ ]);
15
+
16
+ function planFor(test) {
17
+ if (test.protected || PROTECTED_CLASSES.has(test.classification)) return { action: "KEEP", reason: "PROTECTED_TEST" };
18
+ if (test.classification === "REDUNDANT_CANDIDATE" && test.recommendation === "PROBE_REMOVAL") return { action: "PROBE_REMOVAL", reason: "SEMANTIC_AND_DETERMINISTIC_EVIDENCE_REQUIRED" };
19
+ if (test.classification === "KEEP_DOCUMENTATION_VALUE") return { action: "KEEP", reason: "DOCUMENTATION_VALUE" };
20
+ return { action: "BLOCKED", reason: "INSUFFICIENT_PROVENANCE" };
21
+ }
22
+
23
+ export async function buildPrunePlan({ target, packageRoot, taskId } = {}) {
24
+ const utility = await readTestUtility({ target, packageRoot, taskId });
25
+ const items = utility.value.tests.map((test) => ({ testId: test.testId, file: test.file, name: test.name, ...planFor(test) }));
26
+ return { schemaVersion: 1, taskId, semanticStatus: utility.value.semanticStatus, items, deletionAuthority: false };
27
+ }
28
+
29
+ export async function runPruneProbe({ target, packageRoot, taskId, testId } = {}) {
30
+ const plan = await buildPrunePlan({ target, packageRoot, taskId });
31
+ const item = plan.items.find((candidate) => candidate.testId === testId);
32
+ if (!item) {
33
+ const error = new Error(`Unknown test ID: ${testId}`); error.code = "E_TEST_ID_UNKNOWN"; throw error;
34
+ }
35
+ if (item.action !== "PROBE_REMOVAL") {
36
+ const error = new Error(`Test prune probe blocked for ${testId}: ${item.reason}`);
37
+ error.code = "E_TEST_PRUNE_BLOCKED";
38
+ throw error;
39
+ }
40
+ const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), "forgeloop-prune-probe-"));
41
+ try {
42
+ // Persisted utility records do not grant access outside the project or
43
+ // through symlinks copied into the disposable workspace.
44
+ await assertSafePath(target, item.file);
45
+ await cp(target, temporaryRoot, {
46
+ recursive: true,
47
+ dereference: false,
48
+ filter(source) {
49
+ const relative = path.relative(target, source);
50
+ return relative !== ".forgeloop" && relative !== ".git" && !relative.startsWith(`${path.sep}.forgeloop${path.sep}`) && !relative.startsWith(`${path.sep}.git${path.sep}`) && relative !== "node_modules";
51
+ },
52
+ });
53
+ const dependencies = path.join(target, "node_modules");
54
+ try { await symlink(dependencies, path.join(temporaryRoot, "node_modules"), "junction"); } catch { /* dependencies are optional for static probes */ }
55
+ if (item.file.endsWith(".mjs") || item.file.endsWith(".js")) {
56
+ const candidatePath = await assertSafePath(temporaryRoot, item.file);
57
+ const source = await readFile(candidatePath, "utf8");
58
+ const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59
+ const pattern = new RegExp(`\\b(test|it)\\s*\\(\\s*([\\"'])${escapedName}\\2`);
60
+ if (!pattern.test(source)) {
61
+ return { status: "INCONCLUSIVE", reason: "TEST_UNIT_NOT_LOCATED", testId, liveWorktreeModified: false, temporaryWorkspaceCleaned: true };
62
+ }
63
+ const disabled = source.replace(pattern, (_, keyword, quote) => `${keyword}.skip(${quote}${item.name}${quote}`);
64
+ await writeFile(candidatePath, disabled, "utf8");
65
+ const result = await execFileAsync(process.execPath, ["--test", item.file], {
66
+ cwd: temporaryRoot,
67
+ timeout: 120_000,
68
+ maxBuffer: 2_000_000,
69
+ windowsHide: true,
70
+ });
71
+ return {
72
+ status: "INCONCLUSIVE",
73
+ reason: "COVERAGE_OR_MUTATION_EVIDENCE_UNAVAILABLE",
74
+ testId,
75
+ affectedTestExitCode: 0,
76
+ affectedTestOutput: `${result.stdout ?? ""}${result.stderr ?? ""}`.slice(-8_000),
77
+ liveWorktreeModified: false,
78
+ temporaryWorkspaceCleaned: true,
79
+ };
80
+ }
81
+ return { status: "INCONCLUSIVE", reason: "FRAMEWORK_PROBE_UNSUPPORTED", testId, liveWorktreeModified: false, temporaryWorkspaceCleaned: true };
82
+ } catch (error) {
83
+ return {
84
+ status: "BLOCKED",
85
+ reason: error.code === "ETIMEDOUT" ? "AFFECTED_TEST_TIMEOUT" : "AFFECTED_TEST_FAILED",
86
+ testId,
87
+ error: error.message,
88
+ liveWorktreeModified: false,
89
+ temporaryWorkspaceCleaned: true,
90
+ };
91
+ } finally {
92
+ await rm(temporaryRoot, { recursive: true, force: true });
93
+ }
94
+ }
@@ -0,0 +1,15 @@
1
+ export const TEST_UTILITY_BATCH_SIZE = 8;
2
+
3
+ export function batchTests(tests = [], size = TEST_UTILITY_BATCH_SIZE) {
4
+ const batches = [];
5
+ for (let index = 0; index < tests.length; index += size) batches.push(tests.slice(index, index + size));
6
+ return batches;
7
+ }
8
+
9
+ export function buildTestSemanticState(tests = []) {
10
+ return {
11
+ tests: tests.map(({ testId, file, framework, suite, name, line, sourceSummary, targets, runtimeMs, uniqueBranches, signals }) => ({
12
+ testId, file, framework, suite, name, line, sourceSummary, targets, runtimeMs, uniqueBranches, signals,
13
+ })),
14
+ };
15
+ }
@@ -0,0 +1,40 @@
1
+ import { taskArtifactPath } from "../task-paths.js";
2
+ import { writeJsonArtifact, readJsonArtifact } from "../artifacts.js";
3
+ import { withTaskMutation } from "../task-command.js";
4
+ import { inventoryTests } from "./inventory.js";
5
+ import { buildTestUtilityArtifact } from "./utility.js";
6
+ import { canonicalFingerprint } from "../artifacts.js";
7
+ import { readWorkState } from "../work-state.js";
8
+ import { buildTestUtilityQuestionSet } from "../decision/question-registry.js";
9
+ import { ensureSemanticDecision } from "../decision/service.js";
10
+ import { batchTests, buildTestSemanticState } from "./semantic-state.js";
11
+
12
+ export async function runTestInventory({ projectRoot = process.cwd() } = {}) { return inventoryTests(projectRoot); }
13
+
14
+ export async function runTestUtility({ target, packageRoot, taskId, provider = null } = {}) {
15
+ const inventory = await inventoryTests(target);
16
+ const semanticDecisions = [];
17
+ const state = await readWorkState(target, { packageRoot, taskId });
18
+ for (const [index, batch] of batchTests(inventory.tests).entries()) {
19
+ const questionSet = buildTestUtilityQuestionSet(batch);
20
+ semanticDecisions.push(await ensureSemanticDecision({
21
+ target, packageRoot, taskId, provider,
22
+ decisionId: `test-utility-${index}-${canonicalFingerprint(batch.map((test) => test.testId)).slice(0, 16)}`,
23
+ request: {
24
+ decisionKind: "TEST_UTILITY", questionSetId: questionSet.id, questionSet,
25
+ state: { lifecycle: state, ...buildTestSemanticState(batch) },
26
+ candidateIds: batch.map((test) => test.testId),
27
+ candidateSetFingerprint: canonicalFingerprint(batch.map((test) => test.testId)),
28
+ },
29
+ }));
30
+ }
31
+ return withTaskMutation(target, { taskId, packageRoot }, "test-utility", async (ctx) => {
32
+ const artifact = buildTestUtilityArtifact({ taskId: ctx.taskId, inventory, semanticDecisions: semanticDecisions.map((decision) => decision.artifact) });
33
+ const written = await writeJsonArtifact(target, taskArtifactPath(ctx.taskId, "testUtility"), artifact, "test-utility", packageRoot, { taskId: ctx.taskId, operation: "test-utility" });
34
+ return { path: written.path, artifact, inventory };
35
+ });
36
+ }
37
+
38
+ export async function readTestUtility({ target, packageRoot, taskId } = {}) {
39
+ return readJsonArtifact(target, taskArtifactPath(taskId, "testUtility"), "test-utility", packageRoot);
40
+ }
@@ -0,0 +1,50 @@
1
+ import { canonicalFingerprint } from "../artifacts.js";
2
+
3
+ function classify(test, judgment = null, confidence = 1) {
4
+ const signal = test.signals ?? {};
5
+ if (signal.requiredBehavior) return { classification: "KEEP_REQUIRED", recommendation: "KEEP", protected: true };
6
+ if (signal.security) return { classification: "KEEP_RISK_GUARD", recommendation: "KEEP", protected: true };
7
+ if (signal.criticalPath) return { classification: "KEEP_RECOVERY_INVARIANT", recommendation: "KEEP", protected: true };
8
+ if (signal.migration) return { classification: "KEEP_MIGRATION_COMPATIBILITY", recommendation: "KEEP", protected: true };
9
+ if (signal.publicApi) return { classification: "KEEP_UNIQUE", recommendation: "KEEP", protected: true };
10
+ if (signal.release) return { classification: "KEEP_RELEASE_SMOKE", recommendation: "KEEP", protected: true };
11
+ if (signal.platformSpecific) return { classification: "KEEP_PLATFORM_BEHAVIOR", recommendation: "KEEP", protected: true };
12
+ if (signal.integration) return { classification: "KEEP_INTEGRATION_GUARD", recommendation: "KEEP", protected: true };
13
+ if (!judgment || confidence < 0.75) return { classification: "UNKNOWN", recommendation: "BLOCKED", protected: false };
14
+ if (judgment.protocol_invariant) return { classification: "KEEP_RECOVERY_INVARIANT", recommendation: "KEEP", protected: true };
15
+ if (judgment.semantic_duplicate) {
16
+ return {
17
+ classification: "REDUNDANT_CANDIDATE",
18
+ recommendation: judgment.requires_system_two_review ? "BLOCKED" : "PROBE_REMOVAL",
19
+ protected: false,
20
+ };
21
+ }
22
+ if (judgment.likely_obsolete) return { classification: "OBSOLETE_CANDIDATE", recommendation: "BLOCKED", protected: false };
23
+ if (judgment.likely_flaky_low_signal) return { classification: "FLAKY_LOW_SIGNAL", recommendation: "BLOCKED", protected: false };
24
+ if (judgment.expensive_relative_to_signal) return { classification: "EXPENSIVE_LOW_SIGNAL", recommendation: "BLOCKED", protected: false };
25
+ if (judgment.documentation_value) return { classification: "KEEP_DOCUMENTATION_VALUE", recommendation: "KEEP", protected: false };
26
+ if (judgment.integration_value) return { classification: "KEEP_INTEGRATION_GUARD", recommendation: "KEEP", protected: true };
27
+ if (judgment.has_unique_semantic_intent) return { classification: "KEEP_UNIQUE", recommendation: "KEEP", protected: false };
28
+ return { classification: "UNKNOWN", recommendation: "BLOCKED", protected: false };
29
+ }
30
+
31
+ export function buildTestUtilityArtifact({ taskId, inventory, semanticStatus = "NOT_REQUESTED", semanticDecision = null, semanticDecisions = [] } = {}) {
32
+ const decisions = semanticDecisions.length > 0 ? semanticDecisions : (semanticDecision ? [semanticDecision] : []);
33
+ const byTest = new Map(decisions.flatMap((decision) => Object.entries(decision.decision?.tests ?? {}).map(([testId, judgment]) => [testId, {
34
+ judgment, confidence: Math.min(...Object.keys(judgment).map((key) => decision.confidence?.[`test_${(decision.decision.candidateIds ?? []).indexOf(testId)}_${key}`] ?? 0)),
35
+ }])));
36
+ return {
37
+ schemaVersion: 1,
38
+ protocolVersion: 1,
39
+ taskId,
40
+ generatedAt: new Date().toISOString(),
41
+ inventoryFingerprint: canonicalFingerprint(inventory),
42
+ semanticStatus: decisions.length > 0 ? "PROVIDER_REPORTED" : semanticStatus,
43
+ ...(decisions.length > 0 ? {
44
+ ...(decisions.length === 1 ? { decisionId: decisions[0].decisionId, semanticDecisionFingerprint: canonicalFingerprint(decisions[0]) } : {}),
45
+ decisionIds: decisions.map((decision) => decision.decisionId),
46
+ semanticDecisionFingerprints: decisions.map((decision) => canonicalFingerprint(decision)),
47
+ } : {}),
48
+ tests: (inventory?.tests ?? []).map((test) => ({ ...test, ...classify(test, byTest.get(test.testId)?.judgment, byTest.get(test.testId)?.confidence ?? 0) })),
49
+ };
50
+ }
package/src/core/trace.js CHANGED
@@ -1,5 +1,5 @@
1
- import { buildTaskSnapshot } from "./task-snapshot.js";
2
- import { diagnosisEventsForTask } from "./diagnosis-model.js";
1
+ import { withTaskSnapshot } from "./task-snapshot.js";
2
+ import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
3
3
  import {
4
4
  assertDiagnosticCaseDetails,
5
5
  assertInterventionDetails,
@@ -51,6 +51,7 @@ const EVENT_CATEGORY_MAP = Object.freeze({
51
51
  TASK_RECOVERY_RECORDED: "recovery",
52
52
  TASK_RECOVERY_RESUMED: "recovery",
53
53
  OPERATOR_RECOVERY_RECORDED: "recovery",
54
+ TASK_ABANDONED: "recovery",
54
55
  LEGACY_RECOVERY_MIGRATION_RECORDED: "recovery",
55
56
  GATE_SATISFIED: "policy",
56
57
  COMPLETION_VALIDATED: "completion",
@@ -82,6 +83,13 @@ const LIFECYCLE_TRANSITIONS = Object.freeze([
82
83
  "COMPLETION_REJECTED",
83
84
  ]);
84
85
 
86
+ const STATIC_EVENT_SUMMARIES = Object.freeze({
87
+ TASK_RECOVERY_RESUMED: "Task resumed after recovery",
88
+ OPERATOR_RECOVERY_RECORDED: "Operator recovery recorded",
89
+ TASK_ABANDONED: "Task explicitly abandoned",
90
+ LEGACY_RECOVERY_MIGRATION_RECORDED: "Legacy recovery migration recorded",
91
+ });
92
+
85
93
  export function eventCategory(eventType) {
86
94
  return EVENT_CATEGORY_MAP[eventType] ?? "integrity";
87
95
  }
@@ -93,6 +101,8 @@ export function timestampQuality(event) {
93
101
 
94
102
  function eventSummary(event) {
95
103
  const d = event.details ?? {};
104
+ const staticSummary = STATIC_EVENT_SUMMARIES[event.event];
105
+ if (staticSummary) return staticSummary;
96
106
  switch (event.event) {
97
107
  case "TASK_RECEIVED":
98
108
  return `Task received${d.taskId ? ` (${d.taskId})` : ""}`;
@@ -132,12 +142,6 @@ function eventSummary(event) {
132
142
  return "Continuity checkpoint recorded";
133
143
  case "TASK_RECOVERY_RECORDED":
134
144
  return `Task recovery recorded (${d.classification ?? "unknown"})`;
135
- case "TASK_RECOVERY_RESUMED":
136
- return "Task resumed after recovery";
137
- case "OPERATOR_RECOVERY_RECORDED":
138
- return "Operator recovery recorded";
139
- case "LEGACY_RECOVERY_MIGRATION_RECORDED":
140
- return "Legacy recovery migration recorded";
141
145
  case "COMPLETION_VALIDATED":
142
146
  return "Completion validated";
143
147
  case "COMPLETION_REJECTED":
@@ -197,23 +201,6 @@ export function normalizeProtocolEvent(event, context = {}) {
197
201
  };
198
202
  }
199
203
 
200
- function lifecycleTransitions(events) {
201
- const transitions = [];
202
- let lastPhaseEvent = null;
203
- for (const event of events) {
204
- if (!LIFECYCLE_TRANSITIONS.includes(event.event)) continue;
205
- transitions.push({
206
- sequence: event.seq,
207
- at: event.at,
208
- type: event.event,
209
- details: event.details ?? {},
210
- previous: lastPhaseEvent?.type ?? null,
211
- });
212
- lastPhaseEvent = event;
213
- }
214
- return transitions;
215
- }
216
-
217
204
  function projectChecks(snapshot) {
218
205
  const state = snapshot.state;
219
206
  const attemptsByCheck = new Map();
@@ -285,7 +272,8 @@ function projectChecks(snapshot) {
285
272
  }
286
273
 
287
274
  function legacyDiagnoses(events, taskId) {
288
- return diagnosisEventsForTask(events, taskId).map((event) => ({
275
+ return [...ledgerEventsOfTypes(events, ["DIAGNOSIS_RECORDED"])]
276
+ .filter(event => !taskId || event.taskId === taskId).map((event) => ({
289
277
  sequence: event.seq,
290
278
  at: event.at,
291
279
  sourceModel: "LEGACY_DIAGNOSIS_V1",
@@ -353,35 +341,53 @@ const PHASE_DERIVATIONS = Object.freeze({
353
341
  INTERVENTION_RECORDED: () => "CORRECTING",
354
342
  });
355
343
 
356
- function reconstructPhaseChronology(events) {
344
+ function phaseStep(event, currentPhase) {
345
+ const milestone = PHASE_MILESTONES[event.event];
346
+ if (milestone) return { phase: milestone.phase, quality: milestone.quality };
347
+ const derive = PHASE_DERIVATIONS[event.event];
348
+ const derived = derive ? derive(event.details ?? {}) : null;
349
+ return { phase: derived ?? currentPhase, quality: derived || currentPhase ? "derived" : "unknown" };
350
+ }
351
+
352
+ function orderedSequences(events) {
353
+ let previous = -Infinity;
354
+ return events.every(event => {
355
+ if (!Number.isFinite(event.seq) || event.seq <= previous) return false;
356
+ previous = event.seq;
357
+ return true;
358
+ });
359
+ }
360
+
361
+ function* phasedEvents(events, validatedOrder = false) {
357
362
  let currentPhase = null;
358
- const phaseBySequence = new Map();
359
- const qualityBySequence = new Map();
360
- for (const event of events) {
361
- const milestone = PHASE_MILESTONES[event.event];
362
- if (milestone) {
363
- currentPhase = milestone.phase;
364
- qualityBySequence.set(event.seq, milestone.quality);
365
- } else {
366
- const derive = PHASE_DERIVATIONS[event.event];
367
- const derived = derive ? derive(event.details ?? {}) : null;
368
- if (derived) {
369
- currentPhase = derived;
370
- qualityBySequence.set(event.seq, "derived");
371
- } else {
372
- qualityBySequence.set(event.seq, currentPhase ? "derived" : "unknown");
373
- }
363
+ if (validatedOrder || orderedSequences(events)) {
364
+ for (const event of events) {
365
+ const step = phaseStep(event, currentPhase);
366
+ currentPhase = step.phase;
367
+ yield { event, ...step };
374
368
  }
375
- phaseBySequence.set(event.seq, currentPhase);
369
+ return;
376
370
  }
377
- return { phaseBySequence, qualityBySequence };
371
+ // Legacy duplicate sequence keys retain the original last-write projection.
372
+ const phases = new Map();
373
+ for (const event of events) {
374
+ const step = phaseStep(event, currentPhase);
375
+ currentPhase = step.phase;
376
+ phases.set(event.seq, step);
377
+ }
378
+ for (const event of events) yield { event, ...phases.get(event.seq) };
378
379
  }
379
380
 
380
381
  export function historyQualityFor({ snapshot, normalizedEvents }) {
382
+ return historyQualityFromSummary(snapshot, normalizedEvents.length,
383
+ normalizedEvents.some((event) => event.timestampQuality !== "authoritative"));
384
+ }
385
+
386
+ function historyQualityFromSummary(snapshot, eventCount, hasLegacyTimestamp) {
381
387
  const reasons = [];
382
- const hasLedger = normalizedEvents.length > 0;
388
+ const hasLedger = eventCount > 0;
383
389
  if (!hasLedger) reasons.push("LEDGER_ABSENT");
384
- if (normalizedEvents.some((event) => event.timestampQuality !== "authoritative")) {
390
+ if (hasLegacyTimestamp) {
385
391
  reasons.push("LEGACY_PHASE_TIMESTAMPS_UNAVAILABLE");
386
392
  }
387
393
  if (snapshot.state && Number.isInteger(snapshot.state.verificationCycle) && snapshot.state.verificationCycle > 1
@@ -392,54 +398,69 @@ export function historyQualityFor({ snapshot, normalizedEvents }) {
392
398
  return { level, reasons };
393
399
  }
394
400
 
395
- export async function buildTaskTrace({ target, packageRoot, taskId = null, eventsPath = null } = {}) {
396
- const snapshot = await buildTaskSnapshot({ target, packageRoot, taskId, eventsPath });
397
- const artifactPath = eventsPath ?? ".forgeloop/task-state/<task-key>/events.ndjson";
401
+ // History supplies an output collector only. Complete snapshot auditing and
402
+ // every diagnostic/action projection below remain independent of its selection.
403
+ export async function buildTaskTrace({ target, packageRoot, taskId = null, eventsPath = null } = {}, eventProjection = null) {
404
+ return withTaskSnapshot({ target, packageRoot, taskId, eventsPath }, snapshot =>
405
+ buildTraceFromSnapshot({ target, packageRoot, taskId, eventsPath, snapshot, eventProjection }));
406
+ }
398
407
 
399
- const taskEvents = taskId ? snapshot.events.filter((event) => !event.taskId || event.taskId === taskId) : snapshot.events;
400
- const { phaseBySequence, qualityBySequence } = reconstructPhaseChronology(taskEvents);
408
+ function projectTraceEvents(taskEvents, artifactPath, eventProjection, validatedOrder) {
401
409
  const normalizedEvents = [];
402
- for (const event of taskEvents) {
403
- normalizedEvents.push(normalizeProtocolEvent(event, {
404
- phase: phaseBySequence.get(event.seq) ?? null,
405
- artifactPath,
406
- phaseQuality: qualityBySequence.get(event.seq) ?? "unknown",
407
- }));
408
- }
409
-
410
- const integrity = {
411
- valid: snapshot.integrity.valid,
412
- errors: snapshot.integrity.errors,
413
- };
414
- const historyQuality = historyQualityFor({ snapshot, normalizedEvents });
415
- const diagnostics = structuredDiagnostics(taskEvents);
416
-
417
- const failureSignatures = projectFailureSignatures({ state: snapshot.state, events: taskEvents });
418
- const failureSurfaces = projectFailureSurfaces({ state: snapshot.state, events: taskEvents });
419
-
410
+ const transitions = [];
411
+ let lastPhaseEvent = null;
420
412
  const executions = [];
421
413
  const seenExecutions = new Set();
422
- for (const event of taskEvents) {
423
- const d = event.details ?? {};
424
- const executionId = d.executionId ?? d.executionRef ?? null;
425
- if (!executionId || seenExecutions.has(executionId)) continue;
426
- seenExecutions.add(executionId);
427
- executions.push({
428
- executionId,
429
- sequence: event.seq,
430
- at: event.at,
431
- status: d.status ?? null,
432
- exitCode: d.exitCode ?? null,
433
- resolution: d.resolution ?? null,
414
+ const evidenceSources = new Map();
415
+ const recovery = [];
416
+ const completionAttempts = [];
417
+ const continuity = [];
418
+ let validatedAt = null;
419
+ let rejectedAt = null;
420
+ let actionEventCount = 0;
421
+ let reconciliationCount = 0;
422
+ let totalEventCount = 0;
423
+ let hasLegacyTimestamp = false;
424
+ for (const { event, phase, quality } of phasedEvents(taskEvents, validatedOrder)) {
425
+ const normalized = normalizeProtocolEvent(event, {
426
+ phase: phase ?? null,
427
+ artifactPath,
428
+ phaseQuality: quality ?? "unknown",
434
429
  });
435
- }
430
+ totalEventCount += 1;
431
+ hasLegacyTimestamp ||= normalized.timestampQuality !== "authoritative";
432
+ if (eventProjection) eventProjection.add(normalized);
433
+ else normalizedEvents.push(normalized);
434
+
435
+ if (LIFECYCLE_TRANSITIONS.includes(event.event)) {
436
+ transitions.push({
437
+ sequence: event.seq,
438
+ at: event.at,
439
+ type: event.event,
440
+ details: event.details ?? {},
441
+ previous: lastPhaseEvent?.type ?? null,
442
+ });
443
+ lastPhaseEvent = event;
444
+ }
445
+
446
+ const details = event.details ?? {};
447
+ const executionId = details.executionId ?? details.executionRef ?? null;
448
+ if (executionId && !seenExecutions.has(executionId)) {
449
+ seenExecutions.add(executionId);
450
+ executions.push({
451
+ executionId,
452
+ sequence: event.seq,
453
+ at: event.at,
454
+ status: details.status ?? null,
455
+ exitCode: details.exitCode ?? null,
456
+ resolution: details.resolution ?? null,
457
+ });
458
+ }
436
459
 
437
- const evidenceSources = new Map();
438
- for (const event of taskEvents) {
439
460
  const refs = [
440
- ...(Array.isArray(event.details?.evidenceRefs) ? event.details.evidenceRefs : []),
441
- ...(Array.isArray(event.details?.hypotheses)
442
- ? event.details.hypotheses.flatMap((hypothesis) => hypothesis.evidenceRefs ?? [])
461
+ ...(Array.isArray(details.evidenceRefs) ? details.evidenceRefs : []),
462
+ ...(Array.isArray(details.hypotheses)
463
+ ? details.hypotheses.flatMap((hypothesis) => hypothesis.evidenceRefs ?? [])
443
464
  : []),
444
465
  ];
445
466
  for (const ref of refs) {
@@ -447,19 +468,59 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
447
468
  if (!evidenceSources.has(ref)) evidenceSources.set(ref, { ref, sources: [] });
448
469
  if (!evidenceSources.get(ref).sources.includes(event.event)) evidenceSources.get(ref).sources.push(event.event);
449
470
  }
471
+
472
+ if (["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "TASK_ABANDONED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event)) {
473
+ recovery.push({ sequence: event.seq, at: event.at, type: event.event, details });
474
+ }
475
+ if (event.event === "COMPLETION_VALIDATED" || event.event === "COMPLETION_REJECTED") {
476
+ completionAttempts.push({ sequence: event.seq, at: event.at, type: event.event });
477
+ if (event.event === "COMPLETION_VALIDATED") validatedAt = event.at ?? null;
478
+ else rejectedAt = event.at ?? null;
479
+ }
480
+ if (event.event.startsWith("ACTION_") || event.event.startsWith("APPROVAL_")) actionEventCount += 1;
481
+ if (event.event === "ACTION_RECONCILED") reconciliationCount += 1;
482
+ if (event.event === "CONTINUITY_RECORDED" || event.event === "CHECKPOINT_RECONCILED") {
483
+ continuity.push({ sequence: event.seq, at: event.at, type: event.event });
484
+ }
450
485
  }
451
- const evidence = [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref));
452
486
 
453
- const recovery = taskEvents
454
- .filter((event) => ["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event))
455
- .map((event) => ({ sequence: event.seq, at: event.at, type: event.event, details: event.details ?? {} }));
487
+ return {
488
+ normalizedEvents,
489
+ totalEventCount,
490
+ hasLegacyTimestamp,
491
+ transitions,
492
+ executions,
493
+ evidence: [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref)),
494
+ recovery,
495
+ completion: { validatedAt, rejectedAt, attempts: completionAttempts },
496
+ continuity,
497
+ actionEventCount,
498
+ reconciliationCount,
499
+ };
500
+ }
501
+
502
+ async function buildTraceFromSnapshot({ target, packageRoot, taskId, eventsPath, snapshot, eventProjection }) {
503
+ const artifactPath = eventsPath ?? ".forgeloop/task-state/<task-key>/events.ndjson";
456
504
 
457
- const completionEvents = taskEvents.filter((event) => ["COMPLETION_VALIDATED", "COMPLETION_REJECTED"].includes(event.event));
458
- const completion = {
459
- validatedAt: completionEvents.findLast((event) => event.event === "COMPLETION_VALIDATED")?.at ?? null,
460
- rejectedAt: completionEvents.findLast((event) => event.event === "COMPLETION_REJECTED")?.at ?? null,
461
- attempts: completionEvents.map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
505
+ const belongsToTask = event => !event.taskId || event.taskId === taskId;
506
+ const taskEvents = taskId && !snapshot.events.every(belongsToTask)
507
+ ? snapshot.events.filter(belongsToTask) : snapshot.events;
508
+ // The private snapshot audit already proved seq === index + 1. Invalid
509
+ // ledgers retain the original duplicate/order compatibility projection.
510
+ const {
511
+ normalizedEvents, totalEventCount, hasLegacyTimestamp, transitions,
512
+ executions, evidence, recovery, completion, continuity, actionEventCount, reconciliationCount,
513
+ } = projectTraceEvents(taskEvents, artifactPath, eventProjection, snapshot.integrity.valid);
514
+
515
+ const integrity = {
516
+ valid: snapshot.integrity.valid,
517
+ errors: snapshot.integrity.errors,
462
518
  };
519
+ const historyQuality = historyQualityFromSummary(snapshot, totalEventCount, hasLegacyTimestamp);
520
+ const diagnostics = structuredDiagnostics(taskEvents);
521
+
522
+ const failureSignatures = projectFailureSignatures({ state: snapshot.state, events: taskEvents });
523
+ const failureSurfaces = projectFailureSurfaces({ state: snapshot.state, events: taskEvents });
463
524
 
464
525
  const actions = snapshot.taskId
465
526
  ? await listActions(target, { packageRoot, taskId: snapshot.taskId })
@@ -477,7 +538,6 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
477
538
  artifact: action,
478
539
  }));
479
540
  }
480
- const actionEvents = taskEvents.filter((event) => event.event.startsWith("ACTION_") || event.event.startsWith("APPROVAL_"));
481
541
  const byState = Object.fromEntries([...new Set(actions.map((action) => action.state))].sort()
482
542
  .map((state) => [state, actions.filter((action) => action.state === state).length]));
483
543
  const byCapability = Object.fromEntries([...new Set(actions.map((action) => action.capability))].sort()
@@ -501,8 +561,8 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
501
561
  || (projection.state === "VERIFIED" && !(projection.authorization.valid && projection.verification.valid))
502
562
  ).length,
503
563
  repeatedIdempotencyAttempts,
504
- reconciliationCount: taskEvents.filter((event) => event.event === "ACTION_RECONCILED").length,
505
- eventCount: actionEvents.length,
564
+ reconciliationCount,
565
+ eventCount: actionEventCount,
506
566
  };
507
567
 
508
568
  return {
@@ -526,8 +586,9 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
526
586
  historyQuality,
527
587
  integrity,
528
588
  artifacts: {},
529
- events: normalizedEvents,
530
- transitions: lifecycleTransitions(taskEvents),
589
+ events: eventProjection ? eventProjection.result() : normalizedEvents,
590
+ ...(eventProjection ? { totalEventCount } : {}),
591
+ transitions,
531
592
  executions,
532
593
  checks: projectChecks(snapshot),
533
594
  evidence,
@@ -537,9 +598,7 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
537
598
  },
538
599
  failureSignatures,
539
600
  failureSurfaces,
540
- continuity: taskEvents
541
- .filter((event) => ["CONTINUITY_RECORDED", "CHECKPOINT_RECONCILED"].includes(event.event))
542
- .map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
601
+ continuity,
543
602
  recovery,
544
603
  policy: {},
545
604
  audit: {},