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
@@ -20,10 +20,11 @@ import { estimateModuleCost, buildDryRunReport, renderDryRunReport, runBudgetGat
20
20
  import { decideModelOverride } from './model-override-decision.js';
21
21
  import { createLogger } from '../panoramic/utils/logger.js';
22
22
  import { groupFilesToModules } from './module-grouper.js';
23
+ import { buildSpecCacheKey, normalizeProjectPath, resolveRegenPlan, resolveSourceTarget } from './regen-plan.js';
23
24
  import { groupFilesByLanguage } from './language-grouper.js';
24
- import { scanFiles } from '../utils/file-scanner.js';
25
+ import { scanFiles, createGitignoreFilter } from '../utils/file-scanner.js';
25
26
  import { LanguageAdapterRegistry } from '../adapters/language-adapter-registry.js';
26
- import { buildDocGraph, runAnchorIntegration, runHyperedgeIntegration, scanStoredModuleSpecs, } from '../panoramic/builders/doc-graph-builder.js';
27
+ import { buildDocGraph, isBatchGenerated, runAnchorIntegration, runHyperedgeIntegration, scanStoredModuleSpecs, } from '../panoramic/builders/doc-graph-builder.js';
27
28
  import { chunkMarkdownFiles } from '../panoramic/anchoring/chunker.js';
28
29
  import { createEmbeddingProvider } from '../panoramic/anchoring/providers/factory.js';
29
30
  import { DOC_NODE_KINDS } from '../panoramic/hyperedges/constants.js';
@@ -37,12 +38,13 @@ import { detectAuth } from '../auth/auth-detector.js';
37
38
  import { orchestrateDocsBundle } from '../panoramic/pipelines/docs-bundle-orchestrator.js';
38
39
  import { generateDebtIntelligence, } from '../panoramic/pipelines/debt-intelligence-pipeline.js';
39
40
  import { BATCH_OUTPUT_SUBDIRS } from '../panoramic/output-filenames.js';
40
- import { buildKnowledgeGraph, writeKnowledgeGraph } from '../panoramic/graph/index.js';
41
+ import { buildKnowledgeGraph, writeKnowledgeGraph, normalizeGraphForWrite } from '../panoramic/graph/index.js';
41
42
  import { buildUnifiedGraph, setCurrentUnifiedGraph, getCurrentUnifiedGraph, } from '../knowledge-graph/index.js';
42
43
  import { buildHtmlTemplate } from '../panoramic/exporters/html-template.js';
43
44
  import { SpecStore } from '../spec-store/index.js';
44
45
  import { createRequire } from 'node:module';
45
- import { resolvePythonImport, resolveTsJsImport, findNearestTsConfig, buildTsConfigContext, } from '../knowledge-graph/import-resolver.js';
46
+ import { resolvePythonImport } from '../knowledge-graph/import-resolver.js';
47
+ import { resolveTsJsImport, findNearestTsConfig, buildTsConfigContext, } from '../core/import-resolver.js';
46
48
  // 从 package.json 读取版本号(避免硬编码)
47
49
  const _require = createRequire(import.meta.url);
48
50
  const SPECTRA_VERSION = _require('../../package.json').version;
@@ -64,6 +66,39 @@ const logger = createLogger('batch-orchestrator');
64
66
  // ============================================================
65
67
  // 内部辅助函数
66
68
  // ============================================================
69
+ /**
70
+ * Feature 182 修复面 4:checkpoint replace 语义(completed/failed 互斥去重)。
71
+ *
72
+ * 背景:F175 给 checkpoint 加了「mustRegen 失效重跑」(fall-through)语义,但 completedModules
73
+ * 仍沿用 append-only 写法——已完成 module 被重跑后会二次 push,导致 resume 进度超 totalModules,
74
+ * 且 completed / failed 可能交叉污染(同一 module 同时出现在两个集合)。
75
+ *
76
+ * 本 helper 以 path 为身份键先剔除两个集合中的同名旧条目,再 push 到目标集合,保证:
77
+ * (1) 同一 module 在每个集合最多出现一次;(2) completed 与 failed 互斥。
78
+ *
79
+ * 注意:helper 内不得有 await——JS 单线程下同步段不被 pLimit 并发交错,
80
+ * 保持纯同步可防未来插入 await 后语义退化为「读-改-写」竞态。
81
+ */
82
+ function upsertCompletedModule(state, entry) {
83
+ state.completedModules = state.completedModules.filter((m) => m.path !== entry.path);
84
+ state.failedModules = state.failedModules.filter((m) => m.path !== entry.path);
85
+ state.completedModules.push(entry);
86
+ }
87
+ function recordFailedModule(state, entry) {
88
+ state.failedModules = state.failedModules.filter((m) => m.path !== entry.path);
89
+ state.completedModules = state.completedModules.filter((m) => m.path !== entry.path);
90
+ state.failedModules.push(entry);
91
+ }
92
+ /**
93
+ * F175 FR-017/EC-009:判定 absPath 是否位于受管 modules/ 输出目录内(孤儿删除 ownership 必要条件2)。
94
+ *
95
+ * 用 path.relative 判定目录归属,禁用字符串 startsWith——否则 `specs/modules-old/...` 这类
96
+ * sibling 目录会被前缀匹配误判为受管目录(目录穿越)。同时校验 .spec.md 后缀。
97
+ */
98
+ function isInManagedOutputDir(absPath, modulesDir) {
99
+ const rel = path.relative(modulesDir, path.resolve(absPath));
100
+ return !rel.startsWith('..') && !path.isAbsolute(rel) && absPath.endsWith('.spec.md');
101
+ }
67
102
  /**
68
103
  * 合并多个语言的 ModuleGraph 用于全局拓扑排序
69
104
  * 仅合并 modules 和 edges,SCC/Mermaid 按语言独立保留(TD-002 选项 C)
@@ -172,7 +207,14 @@ export async function runBatch(projectRoot, options = {}) {
172
207
  const effectiveMode = options.mode ?? 'full';
173
208
  // F5:FR-006 — 日志输出当前 mode
174
209
  logger.info(`[info] batch mode: ${effectiveMode}`);
175
- const { force = false, incremental = false, maxRetries = 3, outputDir = 'specs', } = options;
210
+ const { maxRetries = 3, outputDir = 'specs', } = options;
211
+ // F175 FR-002:唯一默认值来源——对直接调用方兜底解析 RegenPlan(CLI/MCP 已在各自入口解析后传入有效值)。
212
+ // 删除原 `incremental = false` 硬编码,默认增量(regenPlan.incremental)由 resolveRegenPlan 决定(FR-001)。
213
+ const regenPlan = resolveRegenPlan({
214
+ incremental: options.incremental,
215
+ full: options.full,
216
+ force: options.force,
217
+ });
176
218
  const startTime = Date.now();
177
219
  const resolvedRoot = path.resolve(projectRoot);
178
220
  const resolvedOutputDir = path.isAbsolute(outputDir)
@@ -187,7 +229,6 @@ export async function runBatch(projectRoot, options = {}) {
187
229
  const rel = path.relative(resolvedRoot, absPath);
188
230
  return rel.startsWith('..') ? absPath : rel;
189
231
  };
190
- const normalizeProjectPath = (inputPath) => inputPath.split(path.sep).join('/');
191
232
  // 步骤 1:扫描文件获取 languageStats
192
233
  const scanResult = scanFiles(resolvedRoot, { projectRoot: resolvedRoot });
193
234
  const languageStats = scanResult.languageStats;
@@ -237,42 +278,34 @@ export async function runBatch(projectRoot, options = {}) {
237
278
  const moduleGroups = new Map(groupResult.groups.map((g) => [g.name, g]));
238
279
  const rootModuleName = options.grouping?.rootModuleName ?? 'root';
239
280
  const existingStoredSpecs = scanStoredModuleSpecs(resolvedOutputDir, resolvedRoot);
240
- const storedSpecByTarget = new Map(existingStoredSpecs.map((spec) => [spec.sourceTarget, spec]));
281
+ // Feature 182:以 cache key 入索引(languageSplit 组带 `::language` 后缀),与 processOneModule
282
+ // 的 moduleCacheKey 口径对齐;旧 spec 无 sourceTargetKey 时回落纯路径 sourceTarget。
283
+ const storedSpecByTarget = new Map(existingStoredSpecs.map((spec) => [spec.sourceTargetKey ?? spec.sourceTarget, spec]));
284
+ // F175:全量逃生口(--full / --force 合并为 regenPlan.full)打可观测日志,
285
+ // 替代原 fallbackReason='force-enabled' 内部信号(W-1 取舍)。
286
+ if (regenPlan.full) {
287
+ logger.info(`[regen] full regeneration (source=${regenPlan.source})`);
288
+ }
289
+ // F175:仅增量路径(regenPlan.incremental)调用 DeltaRegenerator;full 路径直接走全量。
241
290
  let deltaReport;
242
- if (incremental) {
243
- if (force) {
244
- deltaReport = {
245
- title: 'Delta Regeneration Report',
246
- generatedAt: new Date().toISOString(),
247
- projectRoot: resolvedRoot,
248
- mode: 'full',
249
- totalTargets: groupResult.groups.reduce((sum, group) => {
250
- const isRootGroup = group.name === rootModuleName || group.name.startsWith(`${rootModuleName}--`);
251
- return sum + (isRootGroup ? group.files.length : 1);
252
- }, 0),
253
- regenerateTargets: [],
254
- directChanges: [],
255
- propagatedChanges: [],
256
- unchangedTargets: [],
257
- fallbackReason: 'force-enabled',
258
- };
259
- }
260
- else {
261
- const deltaRegenerator = new DeltaRegenerator();
262
- deltaReport = await deltaRegenerator.plan({
263
- projectRoot: resolvedRoot,
264
- dependencyGraph: mergedGraph,
265
- moduleGroups: groupResult.groups,
266
- storedSpecs: existingStoredSpecs,
267
- // Bug 142:传入 effectiveMode 启用 mode-aware cache,
268
- // 旧 spec(无 generatedByMode)或 mode 不匹配时强制 cache miss。
269
- effectiveMode,
270
- });
271
- }
291
+ if (regenPlan.incremental) {
292
+ const deltaRegenerator = new DeltaRegenerator();
293
+ deltaReport = await deltaRegenerator.plan({
294
+ projectRoot: resolvedRoot,
295
+ dependencyGraph: mergedGraph,
296
+ moduleGroups: groupResult.groups,
297
+ storedSpecs: existingStoredSpecs,
298
+ // Bug 142:传入 effectiveMode 启用 mode-aware cache,
299
+ // spec(无 generatedByMode)或 mode 不匹配时强制 cache miss。
300
+ effectiveMode,
301
+ });
272
302
  }
273
303
  const regenerateTargets = new Set(deltaReport?.regenerateTargets ?? []);
274
- const forceFullRegeneration = force || (incremental && deltaReport?.mode === 'full');
275
- const shouldUseIncrementalPlan = incremental && !force && deltaReport?.mode === 'incremental';
304
+ // EC-001 force/full 优先:regenPlan.full 直接全量;增量路径 deltaReport.mode==='full'
305
+ //(首次运行 / 无历史 spec / mode 切换)退化全量仍走 forceFullRegeneration。
306
+ // Feature 182:改为 let——full-resume 时序修复需在 checkpoint 加载后回写(见下方 isResume 块)。
307
+ let forceFullRegeneration = regenPlan.full || (regenPlan.incremental && deltaReport?.mode === 'full');
308
+ let shouldUseIncrementalPlan = regenPlan.incremental && deltaReport?.mode === 'incremental';
276
309
  console.log(`发现 ${mergedGraph.modules.length} 个文件,聚合为 ${processingOrder.length} 个模块`);
277
310
  if (isMultiLang) {
278
311
  console.log(`检测到 ${processedLanguages.length} 种语言: ${processedLanguages.join(', ')}`);
@@ -363,6 +396,18 @@ export async function runBatch(projectRoot, options = {}) {
363
396
  }
364
397
  // 步骤 3:检查是否存在检查点
365
398
  let state = loadCheckpoint(checkpointPath);
399
+ // F175 FR-016/EC-007:full/force 启动时丢弃已加载的 checkpoint completed + failed state,
400
+ // 防止残留 checkpoint 命中导致全量语义被绕过(必须在 completedPaths 构建前清空——
401
+ // 加载时即清,而非在 processOneModule 内"忽略",避免中途崩溃 resume 复用脏 set)。
402
+ // W-1:必须同时清空 failedModules——否则 full 后 summary(:1749 起用 checkedState.failedModules)
403
+ // 仍混入上一轮的旧失败详情,与"full 是干净全量"语义矛盾。
404
+ if (state &&
405
+ regenPlan.full &&
406
+ (state.completedModules.length > 0 || state.failedModules.length > 0)) {
407
+ logger.info(`[regen] full regeneration 丢弃残留 checkpoint state(completed ${state.completedModules.length} / failed ${state.failedModules.length} 模块)`);
408
+ state = { ...state, completedModules: [], failedModules: [] };
409
+ saveCheckpoint(state, checkpointPath);
410
+ }
366
411
  const isResume = state !== null;
367
412
  if (!state) {
368
413
  state = {
@@ -374,7 +419,7 @@ export async function runBatch(projectRoot, options = {}) {
374
419
  processingOrder,
375
420
  completedModules: [],
376
421
  failedModules: [],
377
- forceRegenerate: force,
422
+ forceRegenerate: regenPlan.full,
378
423
  // 多语言扩展字段
379
424
  languageGroups: isMultiLang
380
425
  ? Object.fromEntries(languageGroupsList.map((g) => [g.adapterId, g.files]))
@@ -384,6 +429,17 @@ export async function runBatch(projectRoot, options = {}) {
384
429
  }
385
430
  if (isResume) {
386
431
  console.log(`恢复断点: 已完成 ${state.completedModules.length}/${state.totalModules} 模块`);
432
+ // Feature 182 修复面 5:恢复中断的 --full run 意图。
433
+ // 背景:首轮 --full 把 forceRegenerate=true 落盘到 checkpoint(:660),但 runtime 仅用本轮
434
+ // regenPlan 重算的 forceFullRegeneration——裸增量 resume 时为 false,剩余模块静默降级为增量,
435
+ // 产出半新半旧混合产物。此处从 checkpoint 恢复 full 意图,剩余模块继续全量。
436
+ // 时序安全:两变量消费点全在 processOneModule 内(在此回写之后才被调用)。
437
+ // 注意:full-resume 不清空 completed(:636 清空条件用 regenPlan.full 而非本变量),正是 resume 语义。
438
+ if (state.forceRegenerate && !forceFullRegeneration) {
439
+ forceFullRegeneration = true;
440
+ shouldUseIncrementalPlan = false;
441
+ logger.info('[resume] 检测到中断的 full run,剩余模块继续全量');
442
+ }
387
443
  }
388
444
  // 步骤 4:按模块级拓扑顺序处理
389
445
  // Feature 146 FR-010/FR-011:通过 getActiveCount getter 注入 limit.activeCount,
@@ -443,16 +499,30 @@ export async function runBatch(projectRoot, options = {}) {
443
499
  const group = moduleGroups.get(moduleName);
444
500
  if (!group)
445
501
  return;
446
- if (completedPaths.has(moduleName))
447
- return;
448
502
  const isRoot = moduleName === rootModuleName || moduleName.startsWith(`${rootModuleName}--`);
449
503
  const specPath = path.join(modulesDir, `${moduleName}.spec.md`);
450
- // H4 修复:文件级降级场景下 moduleSourceTarget 须与 targetPath 保持一致(文件路径)
451
- // 否则 --incremental 的 regenerateTargets 查询和 storedSpecByTarget 查询全部错位
452
- const hasDirPathConflict = !isRoot && group.files.length === 1 && conflictingDirPaths.has(group.dirPath);
453
- const moduleSourceTarget = hasDirPathConflict
454
- ? normalizeProjectPath(group.files[0])
455
- : normalizeProjectPath(group.dirPath);
504
+ // FR-019:target 口径与 DeltaRegenerator 共用 resolveSourceTarget(文件级降级分支一致),
505
+ // 否则 --incremental 的 regenerateTargets 查询和 storedSpecByTarget 查询全部错位。
506
+ // T020:target 计算前移到 checkpoint 判定之前,供增量 resume 失效判定使用。
507
+ const moduleSourceTarget = resolveSourceTarget(group, conflictingDirPaths, isRoot);
508
+ // Feature 182:cache key(languageSplit 组带 `::language` 后缀),用于 regenerateTargets /
509
+ // storedSpecByTarget / existingVersion 查询,消除同目录多语言组键碰撞;targetPath 与
510
+ // frontmatter 继续用纯路径 moduleSourceTarget(statSync 路径 + panoramic 匹配口径不变)。
511
+ const moduleCacheKey = buildSpecCacheKey(moduleSourceTarget, group);
512
+ // F175 FR-016:checkpoint 判定。full 路径已在加载时清空 completedPaths(必为干净);
513
+ // 增量 resume 下若 checkpoint 命中但本轮 delta 要求重生成该 target,则失效重跑。
514
+ // C-3:root 模块的 regenerateTargets 是文件级(DeltaRegenerator 对 root 按每个文件
515
+ // 产出 snapshot),但 moduleSourceTarget(root 时为 group.dirPath)不在文件级集合里,
516
+ // 用 dirPath 查永远 miss → mustRegen=false → root 文件变更时 checkpoint 命中被错误跳过。
517
+ // 故 root 模块改判 group.files 中任一文件 target 是否命中 regenerateTargets。
518
+ if (completedPaths.has(moduleName)) {
519
+ const mustRegen = shouldUseIncrementalPlan &&
520
+ (isRoot
521
+ ? group.files.some((filePath) => regenerateTargets.has(normalizeProjectPath(filePath)))
522
+ : regenerateTargets.has(moduleCacheKey));
523
+ if (!mustRegen)
524
+ return;
525
+ }
456
526
  const rootTargetsToGenerate = isRoot
457
527
  ? group.files
458
528
  .map((filePath) => normalizeProjectPath(filePath))
@@ -475,7 +545,7 @@ export async function runBatch(projectRoot, options = {}) {
475
545
  else {
476
546
  const shouldGenerate = forceFullRegeneration
477
547
  || (shouldUseIncrementalPlan
478
- ? regenerateTargets.has(moduleSourceTarget)
548
+ ? regenerateTargets.has(moduleCacheKey)
479
549
  : !fs.existsSync(specPath));
480
550
  if (!shouldGenerate) {
481
551
  skipped.push(moduleName);
@@ -589,24 +659,32 @@ export async function runBatch(projectRoot, options = {}) {
589
659
  }
590
660
  successful.push(moduleName);
591
661
  reporter.complete(moduleName, 'success');
592
- checkedState.completedModules.push({
662
+ // Feature 182:replace 语义(失效重跑时去重 + completed/failed 互斥)
663
+ upsertCompletedModule(checkedState, {
593
664
  path: moduleName,
594
665
  specPath: generatedRootSpecs[0],
595
666
  completedAt: new Date().toISOString(),
596
667
  });
597
668
  }
598
669
  else {
599
- // BUG-A 修复:同一 dirPath 下有多个单文件模块时(如 graphify/ 下有 a.py/b.py),
600
- // 使用文件路径避免多个模块覆盖同一个 {dirName}.spec.md;
601
- // 否则仍使用目录路径(每个目录只有一个文件时,目录名才是有意义的模块标识)
602
- // 注意:hasDirPathConflict 已在上方计算(H4 修复)
603
- const targetPath = hasDirPathConflict
604
- ? path.join(resolvedRoot, group.files[0])
605
- : path.join(resolvedRoot, group.dirPath);
670
+ // BUG-A / H4 修复:targetPath 直接由 moduleSourceTarget 推导(FR-019:resolveSourceTarget
671
+ // 已处理"同一 dirPath 多单文件冲突走文件路径"的降级分支),与 sourceTarget 口径保持一致。
672
+ const targetPath = path.join(resolvedRoot, moduleSourceTarget);
606
673
  // genOptions.skipEnrichment 已在 L648 处理 reading/code-only 模式分派
607
674
  const result = await generateSpec(targetPath, {
608
675
  ...genOptions,
609
- existingVersion: storedSpecByTarget.get(moduleSourceTarget)?.version,
676
+ // Feature 182:注入 group.files(语言限定子集,绝对路径),使写侧只分析本语言文件,
677
+ // 与读侧 group.files 文件集口径一致(消除混语言 hash 分叉 + 双倍分析)。
678
+ files: group.files.map((f) => path.join(resolvedRoot, f)),
679
+ existingVersion: storedSpecByTarget.get(moduleCacheKey)?.version,
680
+ // Feature 182 修复 1:仅 languageSplit 组按 moduleName 显式命名输出文件,
681
+ // 避免同目录多语言组(ts / py)都派生为同名 `<dir>.spec.md` 互相覆盖;
682
+ // 非拆分组不传 → basename 派生,存量单语言仓库命名零变化。
683
+ ...(group.languageSplit ? { outputFileName: `${moduleName}.spec.md` } : {}),
684
+ // Feature 182 修复 2:languageSplit 组首写即落入 frontmatter.sourceTargetKey,
685
+ // 消除「post-mutation 依赖 doc-graph re-render 落盘晚于 checkpoint save」崩溃窗口;
686
+ // in-memory moduleSpec.frontmatter 由 generateFrontmatter 返回自然带上。
687
+ sourceTargetKey: group.languageSplit ? moduleCacheKey : undefined,
610
688
  });
611
689
  // Bug 142:非 root 分支累积 input token,供 catch 块统一做预算检查。
612
690
  if (result.costMetadata?.tokenUsage.input) {
@@ -631,7 +709,8 @@ export async function runBatch(projectRoot, options = {}) {
631
709
  successful.push(moduleName);
632
710
  reporter.complete(moduleName, 'success');
633
711
  }
634
- checkedState.completedModules.push({
712
+ // Feature 182:replace 语义(失效重跑时去重 + completed/failed 互斥)
713
+ upsertCompletedModule(checkedState, {
635
714
  path: moduleName,
636
715
  specPath: toProjectPath(path.resolve(result.specPath)),
637
716
  completedAt: new Date().toISOString(),
@@ -686,7 +765,8 @@ export async function runBatch(projectRoot, options = {}) {
686
765
  reason: 'retry-budget-exceeded',
687
766
  };
688
767
  failed.push(failedModule);
689
- checkedState.failedModules.push(failedModule);
768
+ // Feature 182:replace 语义(失效重跑时去重 + completed/failed 互斥)
769
+ recordFailedModule(checkedState, failedModule);
690
770
  reporter.complete(moduleName, 'failed');
691
771
  break;
692
772
  }
@@ -700,7 +780,8 @@ export async function runBatch(projectRoot, options = {}) {
700
780
  degradedToAstOnly: false,
701
781
  };
702
782
  failed.push(failedModule);
703
- checkedState.failedModules.push(failedModule);
783
+ // Feature 182:replace 语义(失效重跑时去重 + completed/failed 互斥)
784
+ recordFailedModule(checkedState, failedModule);
704
785
  reporter.complete(moduleName, 'failed');
705
786
  }
706
787
  }
@@ -745,7 +826,8 @@ export async function runBatch(projectRoot, options = {}) {
745
826
  reason: 'unhandled-exception',
746
827
  };
747
828
  failed.push(failedModule);
748
- checkedState.failedModules.push(failedModule);
829
+ // Feature 182:replace 语义(失效重跑时去重 + completed/failed 互斥)
830
+ recordFailedModule(checkedState, failedModule);
749
831
  try {
750
832
  reporter.complete(moduleName, 'failed');
751
833
  }
@@ -764,10 +846,63 @@ export async function runBatch(projectRoot, options = {}) {
764
846
  let projectDocsResult;
765
847
  let docsBundleManifestPath;
766
848
  let docsBundleProfiles;
767
- // SpecStore 统一查询入口:所有消费方(README、graph、coverage、index、cross-ref)共享此源
849
+ // F175 FR-017/EC-008/EC-009:删除孤儿 spec(源文件已删除的 batch 产物),使增量产物文件集与全量一致。
850
+ // 必须在构造 SpecStore 之前执行 + 从 storedSpecs 集合中剔除已删项(C-2)——否则后续
851
+ // 所有聚合(index/graph/coverage/cross-ref)仍引用陈旧的已删 spec 视图,产生 stale 输出。
852
+ // 仅在增量/全量路径执行;ownership 边界缺一不删:(1) generatedByMode 存在(batch 专属标记)+
853
+ // (2) 位于受管 modules/ 输出目录内(path.relative 防目录穿越,禁字符串 startsWith)+
854
+ // (3) sourceTarget 已无任何在库源文件(EC-008:删除模块最后一个源文件后,空目录仍 existsSync=true,
855
+ // 故不能仅靠目录存在性判定——以当前扫描到的源文件集合为准)。
856
+ let storedSpecsForStore = existingStoredSpecs;
857
+ if (regenPlan.incremental || regenPlan.full) {
858
+ // 当前扫描到的源文件(项目相对,正斜杠口径),用于判定 sourceTarget 是否仍有活跃源文件
859
+ const liveSourceFiles = new Set(scanResult.files.map((f) => normalizeProjectPath(f)));
860
+ const hasLiveSource = (sourceTarget) => {
861
+ const prefix = `${sourceTarget}/`;
862
+ for (const f of liveSourceFiles) {
863
+ if (f === sourceTarget || f.startsWith(prefix))
864
+ return true;
865
+ }
866
+ return false;
867
+ };
868
+ // 已确认删除的孤儿 outputPath 集合,用于从 storedSpecs 中剔除,使 SpecStore 不持有陈旧视图。
869
+ const deletedOrphanPaths = new Set();
870
+ for (const orphan of existingStoredSpecs) {
871
+ if (!isBatchGenerated(orphan))
872
+ continue; // 必要条件1:无 generatedByMode → 跳过(防误删手写 / 单文件 generate 产物)
873
+ if (hasLiveSource(orphan.sourceTarget))
874
+ continue; // 必要条件3:仍有活跃源文件 → 非孤儿
875
+ const absPath = path.isAbsolute(orphan.outputPath)
876
+ ? orphan.outputPath
877
+ : path.join(resolvedRoot, orphan.outputPath);
878
+ if (!isInManagedOutputDir(absPath, modulesDir))
879
+ continue; // 必要条件2:受管 modules/ 目录外 → 跳过
880
+ // 磁盘文件已被本轮其它逻辑清理:视为已删除,从内存视图剔除(保持内存与磁盘一致)。
881
+ if (!fs.existsSync(absPath)) {
882
+ deletedOrphanPaths.add(orphan.outputPath);
883
+ continue;
884
+ }
885
+ // W-1(quality-review):rmSync 是破坏性新操作,权限/OS 错误不应让整个 batch 崩溃。
886
+ // 失败时 warn + 跳过,且【不】加入 deletedOrphanPaths——文件仍在磁盘,内存视图须保留它,
887
+ // 避免"内存说已删、磁盘还在"的不一致。仅删除成功才剔除内存视图。
888
+ try {
889
+ logger.info(`[orphan-cleanup] 删除孤儿 spec: ${orphan.outputPath}`);
890
+ fs.rmSync(absPath, { force: true });
891
+ deletedOrphanPaths.add(orphan.outputPath);
892
+ }
893
+ catch (err) {
894
+ logger.warn(`[orphan-cleanup] 删除孤儿 spec 失败,保守跳过: ${orphan.outputPath}(${err.message})`);
895
+ }
896
+ }
897
+ if (deletedOrphanPaths.size > 0) {
898
+ storedSpecsForStore = existingStoredSpecs.filter((spec) => !deletedOrphanPaths.has(spec.outputPath));
899
+ }
900
+ }
901
+ // SpecStore 统一查询入口:所有消费方(README、graph、coverage、index、cross-ref)共享此源。
902
+ // 使用已剔除孤儿的 storedSpecsForStore,确保后续聚合不引用已删 spec(C-2)。
768
903
  const specStore = new SpecStore({
769
904
  currentSpecs: collectedModuleSpecs,
770
- storedSpecs: existingStoredSpecs,
905
+ storedSpecs: storedSpecsForStore,
771
906
  projectRoot: resolvedRoot,
772
907
  toProjectPath,
773
908
  });
@@ -1115,6 +1250,11 @@ export async function runBatch(projectRoot, options = {}) {
1115
1250
  catch (communityErr) {
1116
1251
  logger.warn(`community-analysis: 社区分析失败,跳过报告生成: ${communityErr instanceof Error ? communityErr.message : String(communityErr)}`);
1117
1252
  }
1253
+ // F175 FR-006/FR-007:写盘前归一化(byte-stable)——nodes/links/hyperedges 确定性排序,
1254
+ // 在追加 semantic edges + 社区分析之后调用,覆盖完整边集。
1255
+ // F179:stripTimestamps:true 使 graph.generatedAt 固定为 epoch,落盘 graph.json 真 byte-stable
1256
+ // (此前 F175 仅在测试读取侧 delete generatedAt,是 over-claim;现在落盘侧直接固定)。
1257
+ normalizeGraphForWrite(graphJson, { stripTimestamps: true });
1118
1258
  // 社区分析完成后写盘(graphJson 已含 degree metadata)
1119
1259
  const graphWrittenPath = writeKnowledgeGraph(graphJson, resolvedOutputDir);
1120
1260
  docGraphPath = toProjectPath(graphWrittenPath);
@@ -1374,6 +1514,7 @@ export async function runBatch(projectRoot, options = {}) {
1374
1514
  docGraphPath,
1375
1515
  coverageReportPath,
1376
1516
  deltaReportPath,
1517
+ deltaReport,
1377
1518
  projectDocs,
1378
1519
  docsBundleManifestPath,
1379
1520
  docsBundleProfiles,
@@ -1616,8 +1757,10 @@ export async function collectPythonCodeSkeletons(projectRoot) {
1616
1757
  // 避免调用方传相对路径 → Map key 与 imports[].resolvedPath 形态不一致
1617
1758
  // → call-resolver buildImportIndex lookup miss
1618
1759
  const resolvedProjectRoot = path.resolve(projectRoot);
1760
+ // F194:构建 .gitignore 过滤器,基准 = resolvedProjectRoot(与 walk 内 path.relative 同口径)
1761
+ const isGitignored = createGitignoreFilter(resolvedProjectRoot);
1619
1762
  const pyFiles = [];
1620
- walkPyFiles(resolvedProjectRoot, pyFiles);
1763
+ walkPyFiles(resolvedProjectRoot, pyFiles, isGitignored, resolvedProjectRoot);
1621
1764
  // Feature 152 P3 T-017:大文件 size guard 提前到 parse 之前(避免 tree-sitter 解析阻塞)
1622
1765
  // 1MB 阈值与 PythonMapper.CALLSITES_MAX_FILE_BYTES 对齐(EC-14)
1623
1766
  const MAX_FILE_BYTES = 1_000_000;
@@ -1695,7 +1838,11 @@ export async function collectPythonCodeSkeletons(projectRoot) {
1695
1838
  }
1696
1839
  return out;
1697
1840
  }
1698
- function walkPyFiles(dir, out) {
1841
+ /**
1842
+ * F194:isGitignored 与 resolvedRoot 由 collectPythonCodeSkeletons 构建并通过参数传入,
1843
+ * 在自写 walk 上叠加 .gitignore 过滤层(保留 PY_SKELETON_IGNORE_DIRS 与点前缀剪枝不变)。
1844
+ */
1845
+ function walkPyFiles(dir, out, isGitignored, resolvedRoot) {
1699
1846
  let entries;
1700
1847
  try {
1701
1848
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -1704,14 +1851,20 @@ function walkPyFiles(dir, out) {
1704
1851
  return;
1705
1852
  }
1706
1853
  for (const entry of entries) {
1854
+ // 基准 = resolvedRoot,path.relative 输出不做 sep 转换(与 file-scanner walkDir 一致)
1855
+ const relPath = path.relative(resolvedRoot, path.join(dir, entry.name));
1707
1856
  if (entry.isDirectory()) {
1708
1857
  if (entry.name.startsWith('.'))
1709
1858
  continue;
1710
1859
  if (PY_SKELETON_IGNORE_DIRS.has(entry.name))
1711
1860
  continue;
1712
- walkPyFiles(path.join(dir, entry.name), out);
1861
+ if (isGitignored(relPath))
1862
+ continue; // 目录命中 .gitignore → 剪枝
1863
+ walkPyFiles(path.join(dir, entry.name), out, isGitignored, resolvedRoot);
1713
1864
  }
1714
1865
  else if (entry.isFile() && (entry.name.endsWith('.py') || entry.name.endsWith('.pyi'))) {
1866
+ if (isGitignored(relPath))
1867
+ continue; // 文件命中 .gitignore → 跳过
1715
1868
  out.push(path.join(dir, entry.name));
1716
1869
  }
1717
1870
  }
@@ -1742,8 +1895,10 @@ export async function collectTsJsCodeSkeletons(projectRoot, options) {
1742
1895
  // Codex P3+P4 复审 C-2 修复:projectRoot 显式 normalize 为绝对路径
1743
1896
  // 避免调用方传相对路径 → Map key 与 imports[].resolvedPath 形态不一致
1744
1897
  const resolvedProjectRoot = path.resolve(projectRoot);
1898
+ // F194:构建 .gitignore 过滤器,基准 = resolvedProjectRoot(与 walk 内 path.relative 同口径)
1899
+ const isGitignored = createGitignoreFilter(resolvedProjectRoot);
1745
1900
  const tsJsFiles = [];
1746
- walkTsJsFiles(resolvedProjectRoot, tsJsFiles);
1901
+ walkTsJsFiles(resolvedProjectRoot, tsJsFiles, isGitignored, resolvedProjectRoot);
1747
1902
  // T-021a:tsconfig context 缓存(by configDir),避免每个文件重复读
1748
1903
  const tsConfigCache = new Map();
1749
1904
  // 大文件 size guard 与 Python 版本对齐(EC-14:1MB 阈值)
@@ -1764,14 +1919,14 @@ export async function collectTsJsCodeSkeletons(projectRoot, options) {
1764
1919
  const skeleton = await adapter.analyzeFile(filePath, {
1765
1920
  extractCallSites: options?.extractCallSites,
1766
1921
  });
1767
- // T-021a:查找最近的 tsconfig.json 并缓存 context
1768
- const nearest = findNearestTsConfig(filePath, resolvedProjectRoot);
1922
+ // T-021a:查找最近的 tsconfig.json 并缓存 context(Feature 181:loader 收口为 configPath 单参)
1923
+ const configPath = findNearestTsConfig(filePath, resolvedProjectRoot);
1769
1924
  let tsConfigContext = null;
1770
- if (nearest) {
1771
- if (!tsConfigCache.has(nearest.configDir)) {
1772
- tsConfigCache.set(nearest.configDir, buildTsConfigContext(nearest.rawConfig, nearest.configDir));
1925
+ if (configPath) {
1926
+ if (!tsConfigCache.has(configPath)) {
1927
+ tsConfigCache.set(configPath, buildTsConfigContext(configPath));
1773
1928
  }
1774
- tsConfigContext = tsConfigCache.get(nearest.configDir) ?? null;
1929
+ tsConfigContext = tsConfigCache.get(configPath) ?? null;
1775
1930
  }
1776
1931
  // 解析 imports[].resolvedPath(EC-10:转绝对路径)
1777
1932
  const resolvedSkeleton = {
@@ -1798,8 +1953,11 @@ export async function collectTsJsCodeSkeletons(projectRoot, options) {
1798
1953
  /**
1799
1954
  * 递归扫描 .ts/.tsx/.js/.jsx 文件(排除产物目录)。
1800
1955
  * 复用 walkPyFiles 的扫描模式,扩展 TS/JS 扩展名集合。
1956
+ *
1957
+ * F194:isGitignored 与 resolvedRoot 由 collectTsJsCodeSkeletons 构建并通过参数传入,
1958
+ * 在自写 walk 上叠加 .gitignore 过滤层(保留 TSJS_SKELETON_IGNORE_DIRS 与点前缀剪枝不变)。
1801
1959
  */
1802
- function walkTsJsFiles(dir, out) {
1960
+ function walkTsJsFiles(dir, out, isGitignored, resolvedRoot) {
1803
1961
  let entries;
1804
1962
  try {
1805
1963
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -1808,12 +1966,16 @@ function walkTsJsFiles(dir, out) {
1808
1966
  return;
1809
1967
  }
1810
1968
  for (const entry of entries) {
1969
+ // 基准 = resolvedRoot,path.relative 输出不做 sep 转换(与 file-scanner walkDir 一致)
1970
+ const relPath = path.relative(resolvedRoot, path.join(dir, entry.name));
1811
1971
  if (entry.isDirectory()) {
1812
1972
  if (entry.name.startsWith('.'))
1813
1973
  continue;
1814
1974
  if (TSJS_SKELETON_IGNORE_DIRS.has(entry.name))
1815
1975
  continue;
1816
- walkTsJsFiles(path.join(dir, entry.name), out);
1976
+ if (isGitignored(relPath))
1977
+ continue; // 目录命中 .gitignore → 剪枝
1978
+ walkTsJsFiles(path.join(dir, entry.name), out, isGitignored, resolvedRoot);
1817
1979
  }
1818
1980
  else if (entry.isFile()) {
1819
1981
  const name = entry.name;
@@ -1821,9 +1983,83 @@ function walkTsJsFiles(dir, out) {
1821
1983
  name.endsWith('.tsx') ||
1822
1984
  name.endsWith('.js') ||
1823
1985
  name.endsWith('.jsx')) {
1986
+ if (isGitignored(relPath))
1987
+ continue; // 文件命中 .gitignore → 跳过
1824
1988
  out.push(path.join(dir, entry.name));
1825
1989
  }
1826
1990
  }
1827
1991
  }
1828
1992
  }
1993
+ /**
1994
+ * Feature 195:全仓纯 AST、零 LLM 的 graph-only 建图。
1995
+ *
1996
+ * 与 runBatch 解耦的姊妹管线——只跑 AST 采集 → unifiedGraph(call graph + depends-on)
1997
+ * + Python 符号节点 → buildKnowledgeGraph → writeKnowledgeGraph,全程不调用任何
1998
+ * spec-gen / enrichment / hyperedge LLM,也不需要认证。
1999
+ *
2000
+ * why 复用 collectPython/collectTsJs 而非 scanFiles:batch 的 unifiedGraph(见 runBatch
2001
+ * 内早期 UnifiedGraph 构建段)正是用这两个采集器构建,复用它们使 graph-only 的 unifiedGraph
2002
+ * 与 batch 口径逐一对齐(扩展名集合 + ignore 规则一致)。不按 languages 过滤,与 batch
2003
+ * unifiedGraph 行为一致。
2004
+ *
2005
+ * why writeKnowledgeGraph 传 stripTimestamps:true:与 batch 写盘一致,剥除 generatedAt
2006
+ * 实时戳,保 byte-stable 与跨 worktree 一致(F193/NFR-002)。
2007
+ */
2008
+ export async function buildAstGraphOnly(projectRoot, options) {
2009
+ const startTime = Date.now();
2010
+ const resolvedRoot = path.resolve(projectRoot);
2011
+ const outputDir = options?.outputDir ?? 'specs';
2012
+ const resolvedOutputDir = path.isAbsolute(outputDir)
2013
+ ? outputDir
2014
+ : path.join(resolvedRoot, outputDir);
2015
+ logger.info('[Feature 195] graph-only 建图:纯 AST / 零 LLM');
2016
+ // 步骤 1:AST 采集(与 batch unifiedGraph 同款采集器,合并 Python + TS/JS)
2017
+ const pythonSkeletons = await collectPythonCodeSkeletons(resolvedRoot);
2018
+ const tsJsSkeletons = await collectTsJsCodeSkeletons(resolvedRoot, {
2019
+ extractCallSites: true,
2020
+ });
2021
+ const codeSkeletons = new Map([...pythonSkeletons, ...tsJsSkeletons]);
2022
+ if (codeSkeletons.size === 0) {
2023
+ // EC-001:无可解析源码 → 仍产出 schema 合法的空图,不崩溃
2024
+ logger.warn('[Feature 195] 未发现可建图的源码(将产出空图)');
2025
+ }
2026
+ // 步骤 2:UnifiedGraph(calls + depends-on,纯 AST)
2027
+ const unifiedGraph = codeSkeletons.size > 0
2028
+ ? buildUnifiedGraph({ projectRoot: resolvedRoot, codeSkeletons })
2029
+ : undefined;
2030
+ // 步骤 3:Python 符号节点(纯 AST,extractionResults 第四路)
2031
+ // EC-003:extractSymbolNodes 内部已 best-effort(单文件失败仍产文件级节点),此处仅兜底捕获
2032
+ let pythonSymbolResults = [];
2033
+ try {
2034
+ const { PythonLanguageAdapter } = await import('../adapters/python-adapter.js');
2035
+ pythonSymbolResults = await new PythonLanguageAdapter().extractSymbolNodes(resolvedRoot);
2036
+ }
2037
+ catch (pyErr) {
2038
+ logger.warn(`[Feature 195] Python 符号提取失败,跳过: ${String(pyErr)}`);
2039
+ }
2040
+ const pythonSymbolCount = pythonSymbolResults.reduce((sum, r) => sum + r.nodes.filter((n) => n.kind === 'component').length, 0);
2041
+ // 步骤 4:合并构建 knowledge graph(不传 docGraph/architectureIR/crossReferenceLinks——
2042
+ // 那三路依赖 spec-gen 产物;EC-004:buildKnowledgeGraph 对缺席数据源 graceful skip)
2043
+ const graphJson = buildKnowledgeGraph({
2044
+ ...(unifiedGraph ? { unifiedGraph } : {}),
2045
+ ...(pythonSymbolResults.length > 0 ? { extractionResults: pythonSymbolResults } : {}),
2046
+ });
2047
+ // 步骤 5:复用 F183 写盘出口(内部 portable 守卫扫描 → normalizeGraphForWrite → 原子写)
2048
+ const graphPath = writeKnowledgeGraph(graphJson, resolvedOutputDir, {
2049
+ stripTimestamps: true,
2050
+ });
2051
+ const callEdgeCount = graphJson.links.filter((e) => e.relation === 'calls').length;
2052
+ const dependsOnEdgeCount = graphJson.links.filter((e) => e.relation === 'depends-on').length;
2053
+ logger.info(`[Feature 195] graph-only 完成:${graphJson.nodes.length} 节点 / ${graphJson.links.length} 边 ` +
2054
+ `(${callEdgeCount} calls, ${dependsOnEdgeCount} depends-on, ${pythonSymbolCount} Python 符号)`);
2055
+ return {
2056
+ graphPath,
2057
+ nodeCount: graphJson.nodes.length,
2058
+ edgeCount: graphJson.links.length,
2059
+ callEdgeCount,
2060
+ dependsOnEdgeCount,
2061
+ pythonSymbolCount,
2062
+ durationMs: Date.now() - startTime,
2063
+ };
2064
+ }
1829
2065
  //# sourceMappingURL=batch-orchestrator.js.map