spectra-cli 4.2.0 → 4.3.0

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 (365) hide show
  1. package/README.md +12 -5
  2. package/dist/.spectra-build-meta.json +9 -0
  3. package/dist/adapters/language-adapter.d.ts +7 -12
  4. package/dist/adapters/language-adapter.d.ts.map +1 -1
  5. package/dist/adapters/python-adapter.d.ts.map +1 -1
  6. package/dist/adapters/python-adapter.js +11 -2
  7. package/dist/adapters/python-adapter.js.map +1 -1
  8. package/dist/batch/batch-orchestrator.d.ts +44 -1
  9. package/dist/batch/batch-orchestrator.d.ts.map +1 -1
  10. package/dist/batch/batch-orchestrator.js +312 -76
  11. package/dist/batch/batch-orchestrator.js.map +1 -1
  12. package/dist/batch/delta-regenerator.d.ts.map +1 -1
  13. package/dist/batch/delta-regenerator.js +21 -28
  14. package/dist/batch/delta-regenerator.js.map +1 -1
  15. package/dist/batch/module-grouper.d.ts +7 -0
  16. package/dist/batch/module-grouper.d.ts.map +1 -1
  17. package/dist/batch/module-grouper.js +2 -0
  18. package/dist/batch/module-grouper.js.map +1 -1
  19. package/dist/batch/regen-plan.d.ts +78 -0
  20. package/dist/batch/regen-plan.d.ts.map +1 -0
  21. package/dist/batch/regen-plan.js +93 -0
  22. package/dist/batch/regen-plan.js.map +1 -0
  23. package/dist/cli/commands/batch.d.ts.map +1 -1
  24. package/dist/cli/commands/batch.js +36 -8
  25. package/dist/cli/commands/batch.js.map +1 -1
  26. package/dist/cli/commands/scaffold-kb.d.ts +9 -0
  27. package/dist/cli/commands/scaffold-kb.d.ts.map +1 -0
  28. package/dist/cli/commands/scaffold-kb.js +179 -0
  29. package/dist/cli/commands/scaffold-kb.js.map +1 -0
  30. package/dist/cli/index.js +24 -5
  31. package/dist/cli/index.js.map +1 -1
  32. package/dist/cli/utils/error-handler.d.ts +0 -6
  33. package/dist/cli/utils/error-handler.d.ts.map +1 -1
  34. package/dist/cli/utils/error-handler.js +0 -13
  35. package/dist/cli/utils/error-handler.js.map +1 -1
  36. package/dist/cli/utils/parse-args.d.ts +43 -3
  37. package/dist/cli/utils/parse-args.d.ts.map +1 -1
  38. package/dist/cli/utils/parse-args.js +98 -12
  39. package/dist/cli/utils/parse-args.js.map +1 -1
  40. package/dist/cli/version-meta.d.ts +16 -0
  41. package/dist/cli/version-meta.d.ts.map +1 -0
  42. package/dist/cli/version-meta.js +28 -0
  43. package/dist/cli/version-meta.js.map +1 -0
  44. package/dist/core/ast-analyzer.d.ts.map +1 -1
  45. package/dist/core/ast-analyzer.js +7 -11
  46. package/dist/core/ast-analyzer.js.map +1 -1
  47. package/dist/core/import-resolver.d.ts +70 -67
  48. package/dist/core/import-resolver.d.ts.map +1 -1
  49. package/dist/core/import-resolver.js +341 -200
  50. package/dist/core/import-resolver.js.map +1 -1
  51. package/dist/core/single-spec-orchestrator.d.ts +21 -0
  52. package/dist/core/single-spec-orchestrator.d.ts.map +1 -1
  53. package/dist/core/single-spec-orchestrator.js +41 -14
  54. package/dist/core/single-spec-orchestrator.js.map +1 -1
  55. package/dist/core/skeleton-hash.d.ts +33 -0
  56. package/dist/core/skeleton-hash.d.ts.map +1 -0
  57. package/dist/core/skeleton-hash.js +76 -0
  58. package/dist/core/skeleton-hash.js.map +1 -0
  59. package/dist/core/tree-sitter-analyzer.d.ts +3 -2
  60. package/dist/core/tree-sitter-analyzer.d.ts.map +1 -1
  61. package/dist/core/tree-sitter-analyzer.js +5 -6
  62. package/dist/core/tree-sitter-analyzer.js.map +1 -1
  63. package/dist/core/tree-sitter-fallback.d.ts +7 -2
  64. package/dist/core/tree-sitter-fallback.d.ts.map +1 -1
  65. package/dist/core/tree-sitter-fallback.js +18 -15
  66. package/dist/core/tree-sitter-fallback.js.map +1 -1
  67. package/dist/extraction/image-extractor.d.ts.map +1 -1
  68. package/dist/extraction/image-extractor.js +3 -1
  69. package/dist/extraction/image-extractor.js.map +1 -1
  70. package/dist/extraction/markdown-extractor.d.ts.map +1 -1
  71. package/dist/extraction/markdown-extractor.js +3 -1
  72. package/dist/extraction/markdown-extractor.js.map +1 -1
  73. package/dist/generator/frontmatter.d.ts +5 -0
  74. package/dist/generator/frontmatter.d.ts.map +1 -1
  75. package/dist/generator/frontmatter.js +4 -0
  76. package/dist/generator/frontmatter.js.map +1 -1
  77. package/dist/kb-mcp/index.d.ts +14 -0
  78. package/dist/kb-mcp/index.d.ts.map +1 -0
  79. package/dist/kb-mcp/index.js +24 -0
  80. package/dist/kb-mcp/index.js.map +1 -0
  81. package/dist/kb-mcp/lib/kb-error.d.ts +21 -0
  82. package/dist/kb-mcp/lib/kb-error.d.ts.map +1 -0
  83. package/dist/kb-mcp/lib/kb-error.js +28 -0
  84. package/dist/kb-mcp/lib/kb-error.js.map +1 -0
  85. package/dist/kb-mcp/lib/kb-locator.d.ts +58 -0
  86. package/dist/kb-mcp/lib/kb-locator.d.ts.map +1 -0
  87. package/dist/kb-mcp/lib/kb-locator.js +85 -0
  88. package/dist/kb-mcp/lib/kb-locator.js.map +1 -0
  89. package/dist/kb-mcp/lib/result-merger.d.ts +35 -0
  90. package/dist/kb-mcp/lib/result-merger.d.ts.map +1 -0
  91. package/dist/kb-mcp/lib/result-merger.js +64 -0
  92. package/dist/kb-mcp/lib/result-merger.js.map +1 -0
  93. package/dist/kb-mcp/server.d.ts +12 -0
  94. package/dist/kb-mcp/server.d.ts.map +1 -0
  95. package/dist/kb-mcp/server.js +29 -0
  96. package/dist/kb-mcp/server.js.map +1 -0
  97. package/dist/kb-mcp/tools/kb-api-lookup.d.ts +28 -0
  98. package/dist/kb-mcp/tools/kb-api-lookup.d.ts.map +1 -0
  99. package/dist/kb-mcp/tools/kb-api-lookup.js +237 -0
  100. package/dist/kb-mcp/tools/kb-api-lookup.js.map +1 -0
  101. package/dist/kb-mcp/tools/kb-doc-lookup.d.ts +20 -0
  102. package/dist/kb-mcp/tools/kb-doc-lookup.d.ts.map +1 -0
  103. package/dist/kb-mcp/tools/kb-doc-lookup.js +92 -0
  104. package/dist/kb-mcp/tools/kb-doc-lookup.js.map +1 -0
  105. package/dist/kb-mcp/tools/kb-search.d.ts +23 -0
  106. package/dist/kb-mcp/tools/kb-search.d.ts.map +1 -0
  107. package/dist/kb-mcp/tools/kb-search.js +126 -0
  108. package/dist/kb-mcp/tools/kb-search.js.map +1 -0
  109. package/dist/knowledge-graph/import-resolver.d.ts +11 -86
  110. package/dist/knowledge-graph/import-resolver.d.ts.map +1 -1
  111. package/dist/knowledge-graph/import-resolver.js +10 -333
  112. package/dist/knowledge-graph/import-resolver.js.map +1 -1
  113. package/dist/knowledge-graph/incremental.d.ts +25 -4
  114. package/dist/knowledge-graph/incremental.d.ts.map +1 -1
  115. package/dist/knowledge-graph/incremental.js +37 -15
  116. package/dist/knowledge-graph/incremental.js.map +1 -1
  117. package/dist/knowledge-graph/index.d.ts +11 -0
  118. package/dist/knowledge-graph/index.d.ts.map +1 -1
  119. package/dist/knowledge-graph/index.js +43 -1
  120. package/dist/knowledge-graph/index.js.map +1 -1
  121. package/dist/knowledge-graph/module-derivation.d.ts +14 -0
  122. package/dist/knowledge-graph/module-derivation.d.ts.map +1 -1
  123. package/dist/knowledge-graph/module-derivation.js +44 -44
  124. package/dist/knowledge-graph/module-derivation.js.map +1 -1
  125. package/dist/knowledge-graph/persistence.d.ts +56 -16
  126. package/dist/knowledge-graph/persistence.d.ts.map +1 -1
  127. package/dist/knowledge-graph/persistence.js +86 -32
  128. package/dist/knowledge-graph/persistence.js.map +1 -1
  129. package/dist/knowledge-graph/query-helpers.d.ts +39 -12
  130. package/dist/knowledge-graph/query-helpers.d.ts.map +1 -1
  131. package/dist/knowledge-graph/query-helpers.js +199 -34
  132. package/dist/knowledge-graph/query-helpers.js.map +1 -1
  133. package/dist/knowledge-graph/relativize.d.ts +51 -0
  134. package/dist/knowledge-graph/relativize.d.ts.map +1 -0
  135. package/dist/knowledge-graph/relativize.js +104 -0
  136. package/dist/knowledge-graph/relativize.js.map +1 -0
  137. package/dist/mcp/agent-context-tools.d.ts +3 -54
  138. package/dist/mcp/agent-context-tools.d.ts.map +1 -1
  139. package/dist/mcp/agent-context-tools.js +141 -189
  140. package/dist/mcp/agent-context-tools.js.map +1 -1
  141. package/dist/mcp/file-nav-tools.d.ts +42 -0
  142. package/dist/mcp/file-nav-tools.d.ts.map +1 -0
  143. package/dist/mcp/file-nav-tools.js +318 -0
  144. package/dist/mcp/file-nav-tools.js.map +1 -0
  145. package/dist/mcp/graph-tools.d.ts +2 -0
  146. package/dist/mcp/graph-tools.d.ts.map +1 -1
  147. package/dist/mcp/graph-tools.js +134 -123
  148. package/dist/mcp/graph-tools.js.map +1 -1
  149. package/dist/mcp/lib/file-nav-helpers.d.ts +128 -0
  150. package/dist/mcp/lib/file-nav-helpers.d.ts.map +1 -0
  151. package/dist/mcp/lib/file-nav-helpers.js +305 -0
  152. package/dist/mcp/lib/file-nav-helpers.js.map +1 -0
  153. package/dist/mcp/lib/telemetry.d.ts +69 -0
  154. package/dist/mcp/lib/telemetry.d.ts.map +1 -0
  155. package/dist/mcp/lib/telemetry.js +113 -0
  156. package/dist/mcp/lib/telemetry.js.map +1 -0
  157. package/dist/mcp/lib/tool-response.d.ts +46 -0
  158. package/dist/mcp/lib/tool-response.d.ts.map +1 -0
  159. package/dist/mcp/lib/tool-response.js +83 -0
  160. package/dist/mcp/lib/tool-response.js.map +1 -0
  161. package/dist/mcp/server.d.ts +13 -2
  162. package/dist/mcp/server.d.ts.map +1 -1
  163. package/dist/mcp/server.js +209 -150
  164. package/dist/mcp/server.js.map +1 -1
  165. package/dist/models/code-skeleton.d.ts +4 -2
  166. package/dist/models/code-skeleton.d.ts.map +1 -1
  167. package/dist/models/code-skeleton.js +2 -1
  168. package/dist/models/code-skeleton.js.map +1 -1
  169. package/dist/models/module-spec.d.ts +18 -0
  170. package/dist/models/module-spec.d.ts.map +1 -1
  171. package/dist/models/module-spec.js +6 -0
  172. package/dist/models/module-spec.js.map +1 -1
  173. package/dist/panoramic/builders/doc-graph-builder.d.ts +13 -0
  174. package/dist/panoramic/builders/doc-graph-builder.d.ts.map +1 -1
  175. package/dist/panoramic/builders/doc-graph-builder.js +20 -0
  176. package/dist/panoramic/builders/doc-graph-builder.js.map +1 -1
  177. package/dist/panoramic/graph/graph-builder.d.ts +72 -2
  178. package/dist/panoramic/graph/graph-builder.d.ts.map +1 -1
  179. package/dist/panoramic/graph/graph-builder.js +257 -51
  180. package/dist/panoramic/graph/graph-builder.js.map +1 -1
  181. package/dist/panoramic/graph/graph-query.d.ts +16 -0
  182. package/dist/panoramic/graph/graph-query.d.ts.map +1 -1
  183. package/dist/panoramic/graph/graph-query.js +49 -1
  184. package/dist/panoramic/graph/graph-query.js.map +1 -1
  185. package/dist/panoramic/graph/index.d.ts +2 -1
  186. package/dist/panoramic/graph/index.d.ts.map +1 -1
  187. package/dist/panoramic/graph/index.js +1 -1
  188. package/dist/panoramic/graph/index.js.map +1 -1
  189. package/dist/panoramic/pipelines/adr-evidence-verifier.d.ts.map +1 -1
  190. package/dist/panoramic/pipelines/adr-evidence-verifier.js +1 -29
  191. package/dist/panoramic/pipelines/adr-evidence-verifier.js.map +1 -1
  192. package/dist/panoramic/query.d.ts +1 -0
  193. package/dist/panoramic/query.d.ts.map +1 -1
  194. package/dist/panoramic/query.js +5 -0
  195. package/dist/panoramic/query.js.map +1 -1
  196. package/dist/scaffold-kb/api-entities-serializer.d.ts +12 -0
  197. package/dist/scaffold-kb/api-entities-serializer.d.ts.map +1 -0
  198. package/dist/scaffold-kb/api-entities-serializer.js +144 -0
  199. package/dist/scaffold-kb/api-entities-serializer.js.map +1 -0
  200. package/dist/scaffold-kb/arbitration.d.ts +44 -0
  201. package/dist/scaffold-kb/arbitration.d.ts.map +1 -0
  202. package/dist/scaffold-kb/arbitration.js +144 -0
  203. package/dist/scaffold-kb/arbitration.js.map +1 -0
  204. package/dist/scaffold-kb/chunk-splitter.d.ts +20 -0
  205. package/dist/scaffold-kb/chunk-splitter.d.ts.map +1 -0
  206. package/dist/scaffold-kb/chunk-splitter.js +309 -0
  207. package/dist/scaffold-kb/chunk-splitter.js.map +1 -0
  208. package/dist/scaffold-kb/doc-graph-builder.d.ts +26 -0
  209. package/dist/scaffold-kb/doc-graph-builder.d.ts.map +1 -0
  210. package/dist/scaffold-kb/doc-graph-builder.js +72 -0
  211. package/dist/scaffold-kb/doc-graph-builder.js.map +1 -0
  212. package/dist/scaffold-kb/entity-extractor.d.ts +40 -0
  213. package/dist/scaffold-kb/entity-extractor.d.ts.map +1 -0
  214. package/dist/scaffold-kb/entity-extractor.js +0 -0
  215. package/dist/scaffold-kb/entity-extractor.js.map +1 -0
  216. package/dist/scaffold-kb/entity-heuristic.d.ts +14 -0
  217. package/dist/scaffold-kb/entity-heuristic.d.ts.map +1 -0
  218. package/dist/scaffold-kb/entity-heuristic.js +107 -0
  219. package/dist/scaffold-kb/entity-heuristic.js.map +1 -0
  220. package/dist/scaffold-kb/entity-matcher.d.ts +20 -0
  221. package/dist/scaffold-kb/entity-matcher.d.ts.map +1 -0
  222. package/dist/scaffold-kb/entity-matcher.js +53 -0
  223. package/dist/scaffold-kb/entity-matcher.js.map +1 -0
  224. package/dist/scaffold-kb/entity-util.d.ts +11 -0
  225. package/dist/scaffold-kb/entity-util.d.ts.map +1 -0
  226. package/dist/scaffold-kb/entity-util.js +23 -0
  227. package/dist/scaffold-kb/entity-util.js.map +1 -0
  228. package/dist/scaffold-kb/evidence-envelope.d.ts +17 -0
  229. package/dist/scaffold-kb/evidence-envelope.d.ts.map +1 -0
  230. package/dist/scaffold-kb/evidence-envelope.js +32 -0
  231. package/dist/scaffold-kb/evidence-envelope.js.map +1 -0
  232. package/dist/scaffold-kb/index.d.ts +20 -0
  233. package/dist/scaffold-kb/index.d.ts.map +1 -0
  234. package/dist/scaffold-kb/index.js +102 -0
  235. package/dist/scaffold-kb/index.js.map +1 -0
  236. package/dist/scaffold-kb/ingest/ingest-core.d.ts +47 -0
  237. package/dist/scaffold-kb/ingest/ingest-core.d.ts.map +1 -0
  238. package/dist/scaffold-kb/ingest/ingest-core.js +231 -0
  239. package/dist/scaffold-kb/ingest/ingest-core.js.map +1 -0
  240. package/dist/scaffold-kb/ingest/office-parser.d.ts +33 -0
  241. package/dist/scaffold-kb/ingest/office-parser.d.ts.map +1 -0
  242. package/dist/scaffold-kb/ingest/office-parser.js +187 -0
  243. package/dist/scaffold-kb/ingest/office-parser.js.map +1 -0
  244. package/dist/scaffold-kb/ingest/url-fetcher.d.ts +43 -0
  245. package/dist/scaffold-kb/ingest/url-fetcher.d.ts.map +1 -0
  246. package/dist/scaffold-kb/ingest/url-fetcher.js +245 -0
  247. package/dist/scaffold-kb/ingest/url-fetcher.js.map +1 -0
  248. package/dist/scaffold-kb/ingester.d.ts +38 -0
  249. package/dist/scaffold-kb/ingester.d.ts.map +1 -0
  250. package/dist/scaffold-kb/ingester.js +261 -0
  251. package/dist/scaffold-kb/ingester.js.map +1 -0
  252. package/dist/scaffold-kb/injection-format.d.ts +25 -0
  253. package/dist/scaffold-kb/injection-format.d.ts.map +1 -0
  254. package/dist/scaffold-kb/injection-format.js +55 -0
  255. package/dist/scaffold-kb/injection-format.js.map +1 -0
  256. package/dist/scaffold-kb/kb-writer.d.ts +13 -0
  257. package/dist/scaffold-kb/kb-writer.d.ts.map +1 -0
  258. package/dist/scaffold-kb/kb-writer.js +69 -0
  259. package/dist/scaffold-kb/kb-writer.js.map +1 -0
  260. package/dist/scaffold-kb/keyword-extract.d.ts +16 -0
  261. package/dist/scaffold-kb/keyword-extract.d.ts.map +1 -0
  262. package/dist/scaffold-kb/keyword-extract.js +49 -0
  263. package/dist/scaffold-kb/keyword-extract.js.map +1 -0
  264. package/dist/scaffold-kb/query-sanitizer.d.ts +28 -0
  265. package/dist/scaffold-kb/query-sanitizer.d.ts.map +1 -0
  266. package/dist/scaffold-kb/query-sanitizer.js +38 -0
  267. package/dist/scaffold-kb/query-sanitizer.js.map +1 -0
  268. package/dist/scaffold-kb/recall-eval.d.ts +48 -0
  269. package/dist/scaffold-kb/recall-eval.d.ts.map +1 -0
  270. package/dist/scaffold-kb/recall-eval.js +55 -0
  271. package/dist/scaffold-kb/recall-eval.js.map +1 -0
  272. package/dist/scaffold-kb/schema-compat.d.ts +22 -0
  273. package/dist/scaffold-kb/schema-compat.d.ts.map +1 -0
  274. package/dist/scaffold-kb/schema-compat.js +32 -0
  275. package/dist/scaffold-kb/schema-compat.js.map +1 -0
  276. package/dist/scaffold-kb/search-core.d.ts +42 -0
  277. package/dist/scaffold-kb/search-core.d.ts.map +1 -0
  278. package/dist/scaffold-kb/search-core.js +89 -0
  279. package/dist/scaffold-kb/search-core.js.map +1 -0
  280. package/dist/scaffold-kb/sqlite-engine.d.ts +58 -0
  281. package/dist/scaffold-kb/sqlite-engine.d.ts.map +1 -0
  282. package/dist/scaffold-kb/sqlite-engine.js +61 -0
  283. package/dist/scaffold-kb/sqlite-engine.js.map +1 -0
  284. package/dist/scaffold-kb/sqlite-writer.d.ts +18 -0
  285. package/dist/scaffold-kb/sqlite-writer.d.ts.map +1 -0
  286. package/dist/scaffold-kb/sqlite-writer.js +76 -0
  287. package/dist/scaffold-kb/sqlite-writer.js.map +1 -0
  288. package/dist/scaffold-kb/tokenizer.d.ts +26 -0
  289. package/dist/scaffold-kb/tokenizer.d.ts.map +1 -0
  290. package/dist/scaffold-kb/tokenizer.js +97 -0
  291. package/dist/scaffold-kb/tokenizer.js.map +1 -0
  292. package/dist/scaffold-kb/types.d.ts +157 -0
  293. package/dist/scaffold-kb/types.d.ts.map +1 -0
  294. package/dist/scaffold-kb/types.js +5 -0
  295. package/dist/scaffold-kb/types.js.map +1 -0
  296. package/dist/utils/file-scanner.d.ts +15 -0
  297. package/dist/utils/file-scanner.d.ts.map +1 -1
  298. package/dist/utils/file-scanner.js +17 -0
  299. package/dist/utils/file-scanner.js.map +1 -1
  300. package/dist/utils/string-distance.d.ts +20 -0
  301. package/dist/utils/string-distance.d.ts.map +1 -0
  302. package/dist/utils/string-distance.js +38 -0
  303. package/dist/utils/string-distance.js.map +1 -0
  304. package/package.json +11 -2
  305. package/plugins/demo-kb-en/.claude-plugin/plugin.json +11 -0
  306. package/plugins/demo-kb-en/.mcp.json +8 -0
  307. package/plugins/demo-kb-en/FIXTURE.json +23 -0
  308. package/plugins/demo-kb-en/FIXTURE.md +53 -0
  309. package/plugins/demo-kb-en/ingest-samples/meeting-notes.md +13 -0
  310. package/plugins/demo-kb-en/kb/api-entities.json +3445 -0
  311. package/plugins/demo-kb-en/kb/chunks.sqlite +0 -0
  312. package/plugins/demo-kb-en/kb/doc-graph.json +113 -0
  313. package/plugins/demo-kb-zh/.claude-plugin/plugin.json +11 -0
  314. package/plugins/demo-kb-zh/.mcp.json +8 -0
  315. package/plugins/demo-kb-zh/FIXTURE.json +23 -0
  316. package/plugins/demo-kb-zh/FIXTURE.md +63 -0
  317. package/plugins/demo-kb-zh/ingest-samples/meeting-notes.md +13 -0
  318. package/plugins/demo-kb-zh/kb/api-entities.json +990 -0
  319. package/plugins/demo-kb-zh/kb/chunks.sqlite +0 -0
  320. package/plugins/demo-kb-zh/kb/doc-graph.json +113 -0
  321. package/plugins/spec-driver/.claude-plugin/plugin.json +1 -1
  322. package/plugins/spec-driver/README.md +33 -1
  323. package/plugins/spec-driver/agents/verify.md +88 -0
  324. package/plugins/spec-driver/config/orchestration.yaml +39 -31
  325. package/plugins/spec-driver/contracts/orchestration-overrides-contract.yaml +27 -0
  326. package/plugins/spec-driver/contracts/orchestration-schema.mjs +272 -227
  327. package/plugins/spec-driver/contracts/wrapper-source-of-truth.yaml +3 -0
  328. package/plugins/spec-driver/hooks/hooks.json +9 -0
  329. package/plugins/spec-driver/hooks/stop-fix-compliance-check.sh +36 -0
  330. package/plugins/spec-driver/lib/delegation-contract.mjs +84 -0
  331. package/plugins/spec-driver/lib/orchestration-resolver.mjs +49 -0
  332. package/plugins/spec-driver/lib/orchestrator.mjs +29 -1
  333. package/plugins/spec-driver/scripts/codex-skills.sh +28 -28
  334. package/plugins/spec-driver/scripts/dev/spike-fix-compliance-e2e.mjs +130 -0
  335. package/plugins/spec-driver/scripts/fix-compliance-judge.mjs +415 -0
  336. package/plugins/spec-driver/scripts/generate-adoption-insights.mjs +48 -1
  337. package/plugins/spec-driver/scripts/goal-loop-cli.mjs +307 -0
  338. package/plugins/spec-driver/scripts/init-project.sh +56 -0
  339. package/plugins/spec-driver/scripts/kb-prequery.mjs +125 -0
  340. package/plugins/spec-driver/scripts/lib/config-schema.mjs +200 -94
  341. package/plugins/spec-driver/scripts/lib/ensure-gitignore.sh +277 -0
  342. package/plugins/spec-driver/scripts/lib/extract-wrapper-body.mjs +136 -0
  343. package/plugins/spec-driver/scripts/lib/fix-compliance-core.mjs +434 -0
  344. package/plugins/spec-driver/scripts/lib/fix-compliance-io.mjs +335 -0
  345. package/plugins/spec-driver/scripts/lib/goal-loop-core.mjs +783 -0
  346. package/plugins/spec-driver/scripts/lib/init-project-output.sh +28 -0
  347. package/plugins/spec-driver/scripts/lib/load-zod.mjs +62 -0
  348. package/plugins/spec-driver/scripts/lib/project-profile-resolver.mjs +141 -49
  349. package/plugins/spec-driver/scripts/lib/project-profile-schema.mjs +88 -54
  350. package/plugins/spec-driver/scripts/postinstall.sh +13 -1
  351. package/plugins/spec-driver/scripts/record-workflow-run.mjs +75 -0
  352. package/plugins/spec-driver/scripts/sync-delegation-contract.mjs +187 -0
  353. package/plugins/spec-driver/scripts/validate-orchestrator-models.mjs +156 -0
  354. package/plugins/spec-driver/scripts/validate-wrapper-sources.mjs +46 -1
  355. package/plugins/spec-driver/skills/spec-driver-feature/SKILL.md +292 -0
  356. package/plugins/spec-driver/skills/spec-driver-fix/SKILL.md +87 -8
  357. package/plugins/spec-driver/skills/spec-driver-implement/SKILL.md +8 -0
  358. package/plugins/spec-driver/skills/spec-driver-resume/SKILL.md +12 -1
  359. package/plugins/spec-driver/skills/spec-driver-story/SKILL.md +20 -0
  360. package/plugins/spec-driver/templates/delegation-contract.md +27 -0
  361. package/plugins/spec-driver/templates/goal-loop-override-template.yaml +284 -0
  362. package/plugins/spec-driver/templates/specify-base/project-context-template.yaml +10 -0
  363. package/plugins/spectra/.claude-plugin/plugin.json +1 -1
  364. package/plugins/spectra/README.md +1 -1
  365. package/templates/module-spec.hbs +1 -0
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * fix-compliance-judge.mjs
5
+ * Feature 208 — fix 模式流程依从性判定 CLI 编排入口(唯一由 hooks.json 挂载的生产路径)
6
+ *
7
+ * 分层契约(research.md D3):本文件是 I/O 编排层,负责
8
+ * 解析参数与 stdin payload → 编排 io 层读取(config/transcript/state)→ 调用 core 纯函数判定
9
+ * → 编排 io 层写入(审计事件 / 阻断计数 / 降级放行的 record-workflow-run 终态事件)
10
+ * → 决定退出码与 stderr 反馈文本。
11
+ *
12
+ * 不变量(contracts/fix-compliance-judge-cli.md):
13
+ * - 零 LLM / 零子代理委派:全程无 `Task(` / 模型 API 调用。
14
+ * - 顶层 try/catch 兜底(FR-013):任何未捕获异常在 hook 模式下转化为 exit 0,不泄漏崩溃退出码。
15
+ * - `--mode report` 恒 exit 0、只打印 verdict JSON、零落盘副作用。
16
+ * - 不读取任务 ID / 任务描述文本作为判据(FR-011)。
17
+ */
18
+
19
+ import process from 'node:process';
20
+ import { fileURLToPath } from 'node:url';
21
+ import fs from 'node:fs';
22
+ import { realpathSync } from 'node:fs';
23
+ import {
24
+ detectFixSkillExpansion,
25
+ extractDelegationsAfter,
26
+ resolveFeatureDirCandidate,
27
+ judgeCompliance,
28
+ MISSING_ACTION_TEXT,
29
+ DUAL_PATH_GUIDANCE,
30
+ GATE_DEGRADED_PREFIX_LINE,
31
+ } from './lib/fix-compliance-core.mjs';
32
+ import {
33
+ readHookPayload,
34
+ readTranscriptEntries,
35
+ findAndParseConfig,
36
+ appendAuditEvent,
37
+ checkFeatureDirOnDisk,
38
+ readArtifactFile,
39
+ loadBlockState,
40
+ saveBlockState,
41
+ resetBlockState,
42
+ } from './lib/fix-compliance-io.mjs';
43
+ import { recordWorkflowRun } from './record-workflow-run.mjs';
44
+
45
+ /** stderr 反馈前缀(FR-010,与既有 stop-task-check.sh 的 `[提醒]` 相区分) */
46
+ const PREFIX_BLOCK = '[FIX-COMPLIANCE]';
47
+ const PREFIX_WARN = '[FIX-COMPLIANCE][WARN]';
48
+ const PREFIX_DEGRADED = '[FIX-COMPLIANCE][GATE-DEGRADED]';
49
+
50
+ /** 会话内不合规阻断上限(FR-006):达到后降级放行 */
51
+ const BLOCK_LIMIT = 2;
52
+
53
+ // ────────────────────────────────────────
54
+ // 参数解析
55
+ // ────────────────────────────────────────
56
+
57
+ export function parseArgs(argv) {
58
+ const args = { mode: 'hook', projectRoot: process.cwd(), transcriptPath: null };
59
+ for (let i = 0; i < argv.length; i += 1) {
60
+ const token = argv[i];
61
+ if (token === '--mode') {
62
+ args.mode = argv[i + 1] ?? args.mode;
63
+ i += 1;
64
+ } else if (token === '--project-root') {
65
+ args.projectRoot = argv[i + 1] ?? args.projectRoot;
66
+ i += 1;
67
+ } else if (token === '--transcript-path') {
68
+ args.transcriptPath = argv[i + 1] ?? args.transcriptPath;
69
+ i += 1;
70
+ }
71
+ }
72
+ if (args.mode !== 'hook' && args.mode !== 'report') args.mode = 'hook';
73
+ return args;
74
+ }
75
+
76
+ // ────────────────────────────────────────
77
+ // stdin 读取(同步,避免异步竞态;hook payload 体量极小)
78
+ // ────────────────────────────────────────
79
+
80
+ function readStdinSync() {
81
+ try {
82
+ return fs.readFileSync(0, 'utf8');
83
+ } catch {
84
+ return '';
85
+ }
86
+ }
87
+
88
+ // ────────────────────────────────────────
89
+ // 判定编排(纯读取,不落盘;hook 与 report 共用)
90
+ // ────────────────────────────────────────
91
+
92
+ /**
93
+ * 编排一次完整判定:读配置 → 读 transcript → 锚定 → 抽取委派/制品 → core 判定。
94
+ * @returns {{
95
+ * enforcement:string, configDegraded:boolean,
96
+ * isFix:boolean, mode:string|null,
97
+ * transcriptDiagnostics:string[],
98
+ * verdict:object|null,
99
+ * }}
100
+ */
101
+ function evaluate(projectRoot, transcriptPath, cfg = null) {
102
+ const config = cfg || findAndParseConfig(projectRoot);
103
+ const enforcement = config.enforcement;
104
+ const configDegraded = config.configDegraded;
105
+ const configDiagnostics = config.diagnostics || [];
106
+
107
+ const { entries, diagnostics: transcriptDiagnostics } = readTranscriptEntries(transcriptPath);
108
+ if (transcriptDiagnostics.length > 0) {
109
+ // transcript 不可用/超限 → FR-013 fail-open(无法得出判定结论)
110
+ return {
111
+ enforcement, configDegraded, isFix: false, mode: null,
112
+ transcriptDiagnostics, verdict: null,
113
+ };
114
+ }
115
+
116
+ const anchor = detectFixSkillExpansion(entries);
117
+ const isFix = anchor.found && anchor.mode === 'fix';
118
+ if (!isFix) {
119
+ return {
120
+ enforcement, configDegraded, isFix: false, mode: anchor.mode,
121
+ transcriptDiagnostics: [], verdict: null,
122
+ };
123
+ }
124
+
125
+ const delegations = extractDelegationsAfter(entries, anchor.anchorLineIndex);
126
+ const candidate = resolveFeatureDirCandidate(entries, anchor.anchorLineIndex);
127
+ const featureDirCheck = checkFeatureDirOnDisk(projectRoot, candidate.path);
128
+ const fixReport = candidate.path
129
+ ? readArtifactFile(projectRoot, `${candidate.path}/fix-report.md`)
130
+ : { exists: false, content: null, nonEmpty: false };
131
+ const verificationReport = candidate.path
132
+ ? readArtifactFile(projectRoot, `${candidate.path}/verification/verification-report.md`)
133
+ : { exists: false, content: null, nonEmpty: false };
134
+
135
+ const verdict = judgeCompliance({
136
+ delegations,
137
+ featureDir: { path: candidate.path, existsOnDisk: featureDirCheck.existsOnDisk },
138
+ fixReport: { exists: fixReport.exists, content: fixReport.content },
139
+ verificationReport: { exists: verificationReport.exists, nonEmpty: verificationReport.nonEmpty },
140
+ enforcement,
141
+ configDegraded,
142
+ diagnostics: configDiagnostics,
143
+ });
144
+
145
+ return {
146
+ enforcement, configDegraded, isFix: true, mode: anchor.mode,
147
+ transcriptDiagnostics: [], verdict,
148
+ };
149
+ }
150
+
151
+ // ────────────────────────────────────────
152
+ // FR-010 反馈文本机械拼装(core 常量拼装,非自由生成)
153
+ // ────────────────────────────────────────
154
+
155
+ /**
156
+ * 由 missing 枚举拼装反馈文本:稳定动作行 + 双路径指引。
157
+ * @param {string[]} missing
158
+ * @param {{ degraded?:boolean, diagnostics?:string[] }} [opts]
159
+ */
160
+ export function buildFeedbackText(missing, opts = {}) {
161
+ const actionLines = (Array.isArray(missing) ? missing : [])
162
+ .map((key) => MISSING_ACTION_TEXT[key])
163
+ .filter(Boolean);
164
+ const segments = [];
165
+ if (opts.degraded) segments.push(GATE_DEGRADED_PREFIX_LINE);
166
+ segments.push(...actionLines);
167
+ segments.push('', DUAL_PATH_GUIDANCE);
168
+ if (Array.isArray(opts.diagnostics) && opts.diagnostics.length > 0) {
169
+ segments.push('', `诊断: ${opts.diagnostics.join(', ')}`);
170
+ }
171
+ return segments.join('\n');
172
+ }
173
+
174
+ // ────────────────────────────────────────
175
+ // 审计事件构造(contracts/fix-compliance-verdict-event.schema.json)
176
+ // ────────────────────────────────────────
177
+
178
+ function buildAuditEvent({ sessionId, enforcement, verdict, blockCount, degraded, extraDiagnostics }) {
179
+ const diag = new Set([
180
+ ...((verdict && verdict.diagnostics) || []),
181
+ ...(extraDiagnostics || []),
182
+ ]);
183
+ return {
184
+ schemaVersion: 1,
185
+ eventType: 'fix-compliance-verdict',
186
+ recordedAt: new Date().toISOString(),
187
+ sessionId,
188
+ enforcement,
189
+ closureForm: verdict ? verdict.closureForm : 'undetermined',
190
+ compliant: verdict ? verdict.compliant : null,
191
+ missing: verdict ? verdict.missing : [],
192
+ blockCount: enforcement === 'block' ? (typeof blockCount === 'number' ? blockCount : null) : null,
193
+ degraded: Boolean(degraded),
194
+ diagnostics: [...diag],
195
+ };
196
+ }
197
+
198
+ // ────────────────────────────────────────
199
+ // hook 模式路由(阻断 / 警告 / 降级放行)
200
+ // ────────────────────────────────────────
201
+
202
+ /**
203
+ * 处理不合规 + block 档:阻断计数路由(FR-006 有界化)。
204
+ * @returns {number} 退出码
205
+ */
206
+ function routeBlock(projectRoot, sessionId, verdict) {
207
+ const loaded = loadBlockState(projectRoot, sessionId);
208
+ const count = loaded.blockCount;
209
+
210
+ if (count < BLOCK_LIMIT) {
211
+ // 未达上限:尝试持久化 N+1 → 成功则硬阻断,失败(存储不可用)则等同已达上限降级放行
212
+ const nextCount = count + 1;
213
+ const saved = saveBlockState(projectRoot, sessionId, {
214
+ blockCount: nextCount,
215
+ degradedRecorded: loaded.degradedRecorded,
216
+ });
217
+ if (saved.ok) {
218
+ appendAuditEvent(projectRoot, buildAuditEvent({
219
+ sessionId, enforcement: 'block', verdict, blockCount: nextCount, degraded: false,
220
+ }));
221
+ process.stderr.write(`${PREFIX_BLOCK} ${buildFeedbackText(verdict.missing)}\n`);
222
+ return 2;
223
+ }
224
+ // 存储不可用 → 无法可靠维持计数,按等同"已达上限"降级放行(research.md D2)
225
+ return releaseDegraded(projectRoot, sessionId, verdict, {
226
+ alreadyRecorded: false,
227
+ storageUnavailable: true,
228
+ });
229
+ }
230
+
231
+ // 已达上限(count >= 2)→ 降级放行
232
+ return releaseDegraded(projectRoot, sessionId, verdict, {
233
+ alreadyRecorded: loaded.degradedRecorded,
234
+ storageUnavailable: false,
235
+ });
236
+ }
237
+
238
+ /**
239
+ * 降级放行:exit 0 + [GATE-DEGRADED] reason + 幂等终态双写(首次)或轻量审计(重复)。
240
+ * @returns {number} 恒 0
241
+ */
242
+ function releaseDegraded(projectRoot, sessionId, verdict, { alreadyRecorded, storageUnavailable }) {
243
+ const extraDiagnostics = storageUnavailable ? ['state-storage-unavailable'] : [];
244
+ const blockCount = BLOCK_LIMIT;
245
+ // 存储不可用无法读写幂等标记 → 允许重复终态(宁可可审计不可静默丢失,research.md D2/D4)
246
+ const shouldWriteTerminal = storageUnavailable || !alreadyRecorded;
247
+
248
+ if (shouldWriteTerminal) {
249
+ try {
250
+ recordWorkflowRun({
251
+ projectRoot,
252
+ workflowId: 'spec-driver-fix',
253
+ runId: sessionId,
254
+ result: 'failed',
255
+ warnings: [`${PREFIX_DEGRADED} fix 会话在 ${BLOCK_LIMIT + 1} 次不合规尝试后降级放行,缺失: ${verdict.missing.join(', ')}`],
256
+ complianceVerdict: {
257
+ closureForm: verdict.closureForm,
258
+ compliant: verdict.compliant,
259
+ missing: verdict.missing,
260
+ degraded: true,
261
+ blockCount,
262
+ },
263
+ });
264
+ } catch {
265
+ // 终态写入失败不得让降级路由崩溃(FR-013 精神)
266
+ }
267
+ // 首次降级成功后置幂等标记(存储可用时才有意义)
268
+ if (!storageUnavailable) {
269
+ saveBlockState(projectRoot, sessionId, { blockCount, degradedRecorded: true });
270
+ }
271
+ }
272
+
273
+ appendAuditEvent(projectRoot, buildAuditEvent({
274
+ sessionId, enforcement: 'block', verdict, blockCount, degraded: true, extraDiagnostics,
275
+ }));
276
+ process.stderr.write(`${PREFIX_DEGRADED} ${buildFeedbackText(verdict.missing, { degraded: true, diagnostics: extraDiagnostics })}\n`);
277
+ return 0;
278
+ }
279
+
280
+ /**
281
+ * FR-013 fail-open 的 loud 半边:判定能力失效时 best-effort 落盘 degraded 诊断事件,
282
+ * 使"漏拦"在事后审计中可被发现而非彻底隐没。写入自身失败不得影响放行(双重兜底)。
283
+ */
284
+ function tryAppendFailOpenEvent(projectRoot, sessionId, enforcement, diagnostics, configDiagnostics = []) {
285
+ try {
286
+ // 合并配置层诊断(如 config-degraded)——配置非法与判定异常同时发生时两类信息都不得丢失
287
+ // (codex implement 审查 W-2,FR-015 可追溯性)
288
+ const merged = [...new Set([
289
+ ...(Array.isArray(configDiagnostics) ? configDiagnostics : []),
290
+ ...(Array.isArray(diagnostics) ? diagnostics : []),
291
+ ])];
292
+ appendAuditEvent(projectRoot, {
293
+ schemaVersion: 1,
294
+ eventType: 'fix-compliance-verdict',
295
+ recordedAt: new Date().toISOString(),
296
+ sessionId: typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : 'unknown',
297
+ enforcement: enforcement === 'warn' ? 'warn' : 'block',
298
+ closureForm: 'undetermined',
299
+ compliant: null,
300
+ missing: [],
301
+ blockCount: null,
302
+ degraded: true,
303
+ diagnostics: merged,
304
+ });
305
+ } catch {
306
+ // 诊断落盘失败不得让 fail-open 路径崩溃
307
+ }
308
+ }
309
+
310
+ /**
311
+ * hook 模式主路由。
312
+ * @returns {number} 退出码
313
+ */
314
+ function runHook(projectRoot, payload) {
315
+ // FR-015 判定顺序:(1) 非抛出式配置解析 →(2) off 立即零接触退出(在任何 transcript 读取之前)
316
+ const cfg = findAndParseConfig(projectRoot);
317
+ if (cfg.enforcement === 'off') return 0;
318
+
319
+ const result = evaluate(projectRoot, payload.transcript_path, cfg);
320
+
321
+ // transcript 不可用/超限 → FR-013 fail-open 放行 + loud 诊断落盘(合并配置层诊断)
322
+ if (result.transcriptDiagnostics.length > 0) {
323
+ tryAppendFailOpenEvent(projectRoot, payload.session_id, cfg.enforcement, result.transcriptDiagnostics, cfg.diagnostics);
324
+ return 0;
325
+ }
326
+ // 非 fix 会话 → 零接触放行(US5:健康路径不产生任何落盘),不 reset 保持零落盘语义
327
+ if (!result.isFix || !result.verdict) return 0;
328
+ // 合规 → 重置该 session 阻断状态(补救成功清零转移,FR-006 增补)后静默放行。
329
+ // 无条件调用(不区分 block/warn):warn 档从不 bump 计数、其状态文件本就不存在,
330
+ // reset 对其为空操作;off 档已在函数入口短路,永不触达此分支。
331
+ if (result.verdict.compliant) {
332
+ resetBlockState(projectRoot, payload.session_id);
333
+ return 0;
334
+ }
335
+
336
+ const sessionId = payload.session_id;
337
+
338
+ if (result.enforcement === 'warn') {
339
+ appendAuditEvent(projectRoot, buildAuditEvent({
340
+ sessionId, enforcement: 'warn', verdict: result.verdict, blockCount: null, degraded: false,
341
+ }));
342
+ process.stderr.write(`${PREFIX_WARN} ${buildFeedbackText(result.verdict.missing)}\n`);
343
+ return 0;
344
+ }
345
+
346
+ // enforcement=block
347
+ return routeBlock(projectRoot, sessionId, result.verdict);
348
+ }
349
+
350
+ // ────────────────────────────────────────
351
+ // report 模式(只读,恒 exit 0,仅 stdout verdict JSON)
352
+ // ────────────────────────────────────────
353
+
354
+ function runReport(projectRoot, transcriptPath) {
355
+ const result = evaluate(projectRoot, transcriptPath);
356
+ const out = {
357
+ mode: result.mode,
358
+ fixSession: result.isFix,
359
+ enforcement: result.enforcement,
360
+ configDegraded: result.configDegraded,
361
+ transcriptDiagnostics: result.transcriptDiagnostics,
362
+ ...(result.verdict || {}),
363
+ };
364
+ process.stdout.write(`${JSON.stringify(out)}\n`);
365
+ return 0;
366
+ }
367
+
368
+ // ────────────────────────────────────────
369
+ // main(顶层 try/catch 兜底 FR-013)
370
+ // ────────────────────────────────────────
371
+
372
+ export function main(argv, stdinRaw) {
373
+ const args = parseArgs(argv);
374
+ try {
375
+ if (args.mode === 'report') {
376
+ // report 优先用 --transcript-path,缺省时回落 stdin payload
377
+ let transcriptPath = args.transcriptPath;
378
+ if (!transcriptPath) {
379
+ const parsed = readHookPayload(stdinRaw);
380
+ transcriptPath = parsed.ok ? parsed.payload.transcript_path : null;
381
+ }
382
+ return runReport(args.projectRoot, transcriptPath);
383
+ }
384
+ // hook 模式:stdin payload 必需
385
+ const parsed = readHookPayload(stdinRaw);
386
+ if (!parsed.ok) {
387
+ // payload 非法 → FR-013 fail-open 放行 + loud 诊断落盘(off 档除外,维持零接触)
388
+ const cfg = findAndParseConfig(args.projectRoot);
389
+ if (cfg.enforcement !== 'off') {
390
+ tryAppendFailOpenEvent(args.projectRoot, null, cfg.enforcement, ['payload-invalid'], cfg.diagnostics);
391
+ }
392
+ return 0;
393
+ }
394
+ return runHook(args.projectRoot, parsed.payload);
395
+ } catch {
396
+ // 任何未预期异常 → fail-open 放行(FR-013)+ best-effort loud 诊断(自身再失败则彻底静默放行)
397
+ try {
398
+ const cfg = findAndParseConfig(args.projectRoot);
399
+ if (cfg.enforcement !== 'off') {
400
+ tryAppendFailOpenEvent(args.projectRoot, null, cfg.enforcement, ['internal-error'], cfg.diagnostics);
401
+ }
402
+ } catch {
403
+ // 连诊断都写不了 → 仍然放行
404
+ }
405
+ return 0;
406
+ }
407
+ }
408
+
409
+ // 仅作为入口脚本直接运行时执行(被 import 时不触发,便于单测)
410
+ if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
411
+ const argv = process.argv.slice(2);
412
+ const stdinRaw = readStdinSync();
413
+ const code = main(argv, stdinRaw);
414
+ process.exit(code);
415
+ }
@@ -18,6 +18,14 @@ import { readJsonArtifact, writeJsonArtifact, writeMarkdownArtifact } from './li
18
18
  const SCHEMA_VERSION = 1;
19
19
  const PRODUCT_ID = 'spec-driver';
20
20
 
21
+ /**
22
+ * 已知的非 summary 事件类型白名单(Feature 208,data-model.md §9)。
23
+ * 这些事件与 workflow-run-summary 共存于同一 .specify/runs/*.jsonl,但不参与 adoption 统计。
24
+ * why 需要白名单:normalizeRunEvent 对非 summary 行返回 null → 原逻辑会计入 invalidLineCount 并
25
+ * 产生"忽略无效 run event"warning,污染 adoption 报告 invalid 统计。白名单命中者静默 skip。
26
+ */
27
+ const KNOWN_NON_SUMMARY_EVENT_TYPES = new Set(['fix-compliance-verdict']);
28
+
21
29
  function parseArgs(argv) {
22
30
  const args = {
23
31
  projectRoot: process.cwd(),
@@ -126,6 +134,10 @@ function readRunLogs(projectRoot) {
126
134
  const line = lines[index];
127
135
  try {
128
136
  const parsed = JSON.parse(line);
137
+ // 已知非 summary 事件类型(如 fix-compliance-verdict)静默 skip:不计 invalid、不产生 warning
138
+ if (isObject(parsed) && KNOWN_NON_SUMMARY_EVENT_TYPES.has(parsed.eventType)) {
139
+ continue;
140
+ }
129
141
  const normalized = normalizeRunEvent(parsed);
130
142
  if (normalized) {
131
143
  events.push(normalized);
@@ -181,12 +193,47 @@ function normalizeRunEvent(entry) {
181
193
  };
182
194
  }
183
195
 
196
+ // 同一 (workflowId, runId) 可能出现多条终态事件:Feature 211 的 compliant-reset 语义下,
197
+ // 同一 session 后续再次耗尽额度降级会重写 workflow-run-summary 终态事件(每个"阻断周期"一条)。
198
+ // JSONL 是 append-only 审计日志,原始事件全部保留;此处仅在统计聚合前按 (workflowId, runId)
199
+ // 去重,取时间最晚的一条作为该 run 的最终结果,避免同一 run 被重复计入 totalRuns 等指标。
200
+ // runId 缺失的事件在 normalizeRunEvent 阶段已被判为无效(不进入本函数),故无需在此特殊处理。
201
+ function dedupeRunEvents(events) {
202
+ const latestByRun = new Map();
203
+ const order = [];
204
+ for (const event of events) {
205
+ const key = `${event.workflowId}\u0000${event.runId}`;
206
+ const existing = latestByRun.get(key);
207
+ if (!existing) {
208
+ latestByRun.set(key, event);
209
+ order.push(key);
210
+ continue;
211
+ }
212
+ // 时间可比时取更晚者;时间相等或缺失时取文件内后出现者(append-only 天然有序)
213
+ const newMs = eventRecencyMs(event);
214
+ const existingMs = eventRecencyMs(existing);
215
+ if (newMs === null || existingMs === null || newMs >= existingMs) {
216
+ latestByRun.set(key, event);
217
+ }
218
+ }
219
+ return order.map((key) => latestByRun.get(key));
220
+ }
221
+
222
+ // 终态事件的"新近度":取 recordedAt / finishedAt 中可解析的最晚时间戳(ms),均缺失则返回 null
223
+ function eventRecencyMs(event) {
224
+ const candidates = [event.recordedAt, event.finishedAt]
225
+ .map((value) => (value ? Date.parse(value) : Number.NaN))
226
+ .filter((value) => !Number.isNaN(value));
227
+ return candidates.length > 0 ? Math.max(...candidates) : null;
228
+ }
229
+
184
230
  function calculateMetrics(events, workflowDefs) {
185
231
  const workflowMap = new Map(
186
232
  (Array.isArray(workflowDefs) ? workflowDefs : [])
187
233
  .filter((workflow) => isObject(workflow) && typeof workflow.id === 'string')
188
234
  .map((workflow) => [workflow.id, workflow]),
189
235
  );
236
+ const dedupedEvents = dedupeRunEvents(events);
190
237
  const grouped = new Map();
191
238
  const rerunPhaseCounts = new Map();
192
239
  const gatePauseCounts = new Map();
@@ -194,7 +241,7 @@ function calculateMetrics(events, workflowDefs) {
194
241
  const phaseDurationCounts = new Map();
195
242
  let runsWithPhaseDurations = 0;
196
243
 
197
- for (const event of events) {
244
+ for (const event of dedupedEvents) {
198
245
  if (!grouped.has(event.workflowId)) {
199
246
  grouped.set(event.workflowId, []);
200
247
  }