@sanqianx/project-knowledge 5.0.24 → 5.0.25

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 (401) hide show
  1. package/.agents/plugins/marketplace.json +20 -20
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +1361 -1353
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/package.json +1 -1
  8. package/_modules/knowledge-engine/src/coordinator.js +48 -3
  9. package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -140
  10. package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +238 -238
  11. package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -26
  12. package/_modules/knowledge-engine/src/runtime/commit-prompt.js +193 -193
  13. package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +394 -394
  14. package/_modules/knowledge-engine/src/runtime/contracts.js +282 -282
  15. package/_modules/knowledge-engine/src/runtime/conversation-store.js +389 -389
  16. package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -228
  17. package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -62
  18. package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +505 -505
  19. package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +400 -400
  20. package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +171 -171
  21. package/_modules/knowledge-engine/src/runtime/scanner.js +298 -298
  22. package/_modules/knowledge-engine/ui/app.js +23 -2
  23. package/_site/README.md +30 -30
  24. package/_site/_test/agent-capture-mcp-ui-test.js +68 -68
  25. package/_site/_test/agent-capture-ui-test.js +101 -101
  26. package/_site/_test/ai-profile-resolver-test.js +137 -137
  27. package/_site/_test/ai-profile-test.js +75 -75
  28. package/_site/_test/ai-vendor-presets-test.js +91 -91
  29. package/_site/_test/automatic-context-test.js +70 -70
  30. package/_site/_test/automation-queue-test.js +14 -14
  31. package/_site/_test/automation-ui-test.js +40 -40
  32. package/_site/_test/background-task-registry-test.js +43 -43
  33. package/_site/_test/baseline-schema-test.js +91 -91
  34. package/_site/_test/bin-cli-test.js +230 -230
  35. package/_site/_test/bridge-adapter-test.js +95 -95
  36. package/_site/_test/bridge-consumer-service-test.js +310 -310
  37. package/_site/_test/candidate-bin-content-test.js +35 -35
  38. package/_site/_test/candidate-product-assets-test.js +13 -13
  39. package/_site/_test/candidate-release-contract-test.js +14 -14
  40. package/_site/_test/capture-hook-dedup-test.js +158 -158
  41. package/_site/_test/cdp-browser-failure-test.js +103 -103
  42. package/_site/_test/chat-claudecodeui-match-test.js +92 -92
  43. package/_site/_test/claude-executable-discovery-test.js +90 -90
  44. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  45. package/_site/_test/claude-workbench-test.js +174 -174
  46. package/_site/_test/cli-instance-management-test.js +62 -62
  47. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  48. package/_site/_test/codex-conversation-projection-test.js +60 -60
  49. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  50. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  51. package/_site/_test/codex-notify-fanout-test.js +141 -134
  52. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  53. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  54. package/_site/_test/commit-conversation-binding-test.js +99 -99
  55. package/_site/_test/commit-evidence-test.js +110 -110
  56. package/_site/_test/configuration-generation-test.js +22 -22
  57. package/_site/_test/context-pack-test.js +200 -200
  58. package/_site/_test/conversation-api-test.js +119 -119
  59. package/_site/_test/conversation-origin-test.js +15 -15
  60. package/_site/_test/conversation-store-test.js +138 -138
  61. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  62. package/_site/_test/desktop-browser-compat-test.js +28 -28
  63. package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
  64. package/_site/_test/embedding-config-test.js +32 -32
  65. package/_site/_test/explicit-commit-processor-test.js +37 -37
  66. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  67. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  68. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  69. package/_site/_test/fixtures/make-git-repos.js +92 -92
  70. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
  71. package/_site/_test/folder-picker-output-test.js +19 -19
  72. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  73. package/_site/_test/fresh-product-data-test.js +56 -56
  74. package/_site/_test/full-integration-e2e-test.js +167 -167
  75. package/_site/_test/git-path-compatibility-test.js +80 -80
  76. package/_site/_test/git-validation-test.js +81 -81
  77. package/_site/_test/glm-ui-refresh-review-test.js +313 -404
  78. package/_site/_test/harness-isolation-test.js +138 -0
  79. package/_site/_test/helpers/cdp-browser.js +383 -0
  80. package/_site/_test/helpers/cdp-network-capture.js +66 -0
  81. package/_site/_test/helpers/find-chrome.js +34 -0
  82. package/_site/_test/helpers/free-port.js +8 -0
  83. package/_site/_test/helpers/logging-ui-fixture.js +131 -0
  84. package/_site/_test/helpers/module-fixture.js +282 -0
  85. package/_site/_test/helpers/parallel-engine-advanced-engine-child.js +45 -0
  86. package/_site/_test/helpers/parallel-engine-advanced-fault-writer.js +171 -0
  87. package/_site/_test/helpers/parallel-engine-advanced-harness.js +245 -0
  88. package/_site/_test/helpers/parallel-engine-advanced-standin-analyzer.js +80 -0
  89. package/_site/_test/helpers/parallel-engine-advanced-ui-host.js +177 -0
  90. package/_site/_test/helpers/parallel-engine-final-harness.js +314 -0
  91. package/_site/_test/helpers/parallel-engine-final-standin-analyzer.js +159 -0
  92. package/_site/_test/helpers/parallel-knowledge-fixture.js +255 -0
  93. package/_site/_test/helpers/parallel-logging-acceptance-fixture.js +111 -0
  94. package/_site/_test/helpers/parallel-logging-acceptance-proxy.js +106 -0
  95. package/_site/_test/helpers/parallel-logging-fix-r2-proxy.js +205 -0
  96. package/_site/_test/helpers/parallel-logging-integration-5025-env.js +432 -0
  97. package/_site/_test/helpers/parallel-memory-analyzer.js +81 -0
  98. package/_site/_test/helpers/parallel-memory-event-bridge.js +128 -0
  99. package/_site/_test/helpers/parallel-memory-shell.js +470 -0
  100. package/_site/_test/helpers/parallel-nav-isolation-env.js +389 -0
  101. package/_site/_test/helpers/parallel-nav-module-ui.js +186 -0
  102. package/_site/_test/helpers/parallel-nav-proxy.js +148 -0
  103. package/_site/_test/helpers/parallel-nav-shell.js +361 -0
  104. package/_site/_test/helpers/parallel-qa-engine-endpoint-guard.js +206 -0
  105. package/_site/_test/helpers/parallel-qa-engine-endpoint-isolate.js +257 -0
  106. package/_site/_test/helpers/parallel-retrieval-followup-corpus.js +293 -0
  107. package/_site/_test/helpers/parallel-retrieval-followup-next-session.js +70 -0
  108. package/_site/_test/helpers/parallel-retrieval-followup-r2-shell.js +737 -0
  109. package/_site/_test/helpers/parallel-retrieval-followup-r2-wb-host.js +82 -0
  110. package/_site/_test/helpers/parallel-retrieval-followup-shell.js +279 -0
  111. package/_site/_test/helpers/parallel-workbench-fixture.js +371 -0
  112. package/_site/_test/helpers/parallel-workbench-host.js +80 -0
  113. package/_site/_test/helpers/parallel-workbench-integration-5025-fixture.js +408 -0
  114. package/_site/_test/helpers/parallel-workbench-integration-5025-harness.js +593 -0
  115. package/_site/_test/helpers/parallel-workbench-integration-5025-host.js +70 -0
  116. package/_site/_test/helpers/parallel-workbench-integration-5025-launcher.js +240 -0
  117. package/_site/_test/helpers/parallel-workbench-integration-5025-runtime-harness.js +630 -0
  118. package/_site/_test/helpers/parallel-workbench-source-map.js +196 -0
  119. package/_site/_test/helpers/spawn-server.js +106 -0
  120. package/_site/_test/helpers/version-visibility-fixture.js +251 -0
  121. package/_site/_test/hook-runtime-endpoint-test.js +88 -88
  122. package/_site/_test/hook-status-repair-api-test.js +152 -152
  123. package/_site/_test/hook-trigger-test.js +85 -85
  124. package/_site/_test/hook-worktree-test.js +42 -42
  125. package/_site/_test/import-preflight-api-test.js +234 -234
  126. package/_site/_test/index-builder-test.js +41 -41
  127. package/_site/_test/index-error-display-contract-test.js +10 -10
  128. package/_site/_test/index-writer-concurrency-test.js +135 -135
  129. package/_site/_test/integration-adapters-test.js +262 -262
  130. package/_site/_test/integration-auto-repair-test.js +113 -113
  131. package/_site/_test/integration-capture-setup-test.js +118 -118
  132. package/_site/_test/integration-intent-test.js +81 -81
  133. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  134. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  135. package/_site/_test/integration-runtime-binding-test.js +473 -473
  136. package/_site/_test/integration-status-scan-test.js +29 -29
  137. package/_site/_test/integration-surface-coverage-test.js +164 -164
  138. package/_site/_test/kb-framework-test.js +57 -57
  139. package/_site/_test/kbpath-follow-test.js +63 -63
  140. package/_site/_test/knowledge-db-test.js +144 -144
  141. package/_site/_test/knowledge-engine-client-test.js +120 -109
  142. package/_site/_test/knowledge-language-control-test.js +170 -170
  143. package/_site/_test/knowledge-maintenance-test.js +40 -40
  144. package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
  145. package/_site/_test/knowledge-query-test.js +54 -54
  146. package/_site/_test/knowledge-retrieval-service-test.js +77 -77
  147. package/_site/_test/knowledge-scopes-test.js +37 -37
  148. package/_site/_test/knowledge-storage-location-test.js +28 -28
  149. package/_site/_test/knowledge-storage-startup-test.js +52 -52
  150. package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
  151. package/_site/_test/launcher-readiness-test.js +80 -80
  152. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  153. package/_site/_test/legacy-routes-removed-test.js +47 -47
  154. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  155. package/_site/_test/live-gateway-test.js +48 -48
  156. package/_site/_test/local-marketplace-test.js +16 -16
  157. package/_site/_test/log-redaction-test.js +58 -58
  158. package/_site/_test/logging-api-test.js +89 -89
  159. package/_site/_test/logging-sse-no-gap-test.js +88 -88
  160. package/_site/_test/logging-ui-test.js +107 -107
  161. package/_site/_test/markdown-delta-overlay-test.js +85 -85
  162. package/_site/_test/markdown-maintenance-api-test.js +75 -75
  163. package/_site/_test/markdown-maintenance-test.js +11 -11
  164. package/_site/_test/mcp-server-test.js +149 -149
  165. package/_site/_test/model-context-windows-test.js +89 -89
  166. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  167. package/_site/_test/module-artifact-integrity-test.js +27 -27
  168. package/_site/_test/module-boundary-test.js +30 -30
  169. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  170. package/_site/_test/module-dynamic-port-test.js +118 -118
  171. package/_site/_test/module-index-proxy-test.js +33 -33
  172. package/_site/_test/module-model-configuration-test.js +44 -44
  173. package/_site/_test/module-process-lifecycle-test.js +35 -35
  174. package/_site/_test/module-stream-proxy-test.js +34 -34
  175. package/_site/_test/module-terminal-restart-test.js +63 -63
  176. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  177. package/_site/_test/native-context-integrations-test.js +86 -86
  178. package/_site/_test/non-release-ci-test.js +34 -34
  179. package/_site/_test/offline-boundary-isolation-test.js +32 -32
  180. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  181. package/_site/_test/package-boundary-test.js +43 -43
  182. package/_site/_test/package-startup-test.js +56 -56
  183. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  184. package/_site/_test/parallel-ci-r9-posix-socket-budget-test.js +156 -0
  185. package/_site/_test/parallel-engine-advanced-eng005-test.js +136 -0
  186. package/_site/_test/parallel-engine-advanced-eng006-test.js +171 -0
  187. package/_site/_test/parallel-engine-advanced-eng007-reconcile-test.js +231 -0
  188. package/_site/_test/parallel-engine-advanced-eng009-test.js +208 -0
  189. package/_site/_test/parallel-engine-advanced-eng010-test.js +213 -0
  190. package/_site/_test/parallel-engine-advanced-eng011-ui-probe.js +459 -0
  191. package/_site/_test/parallel-engine-final-eng002-test.js +255 -0
  192. package/_site/_test/parallel-engine-final-eng007-test.js +269 -0
  193. package/_site/_test/parallel-engine-final-eng011-test.js +197 -0
  194. package/_site/_test/parallel-engine-final-eng012-test.js +354 -0
  195. package/_site/_test/parallel-knowledge-client-error-code-test.js +184 -0
  196. package/_site/_test/parallel-knowledge-engine-test.js +353 -0
  197. package/_site/_test/parallel-knowledge-retrieval-test.js +292 -0
  198. package/_site/_test/parallel-knowledge-vector-test.js +168 -0
  199. package/_site/_test/parallel-linux-ci-r7-log007-day-test.js +110 -0
  200. package/_site/_test/parallel-logging-acceptance-log003-test.js +224 -0
  201. package/_site/_test/parallel-logging-acceptance-log004-test.js +231 -0
  202. package/_site/_test/parallel-logging-acceptance-log005-test.js +208 -0
  203. package/_site/_test/parallel-logging-acceptance-log006-test.js +214 -0
  204. package/_site/_test/parallel-logging-acceptance-log007-test.js +223 -0
  205. package/_site/_test/parallel-logging-acceptance-log008-test.js +180 -0
  206. package/_site/_test/parallel-logging-fix-r2-crash-probe.js +271 -0
  207. package/_site/_test/parallel-logging-fix-r2-gap-test.js +294 -0
  208. package/_site/_test/parallel-logging-fix-r2-normalhook-test.js +330 -0
  209. package/_site/_test/parallel-logging-integration-5025-portability-test.js +554 -0
  210. package/_site/_test/parallel-memory-candidate-test.js +301 -0
  211. package/_site/_test/parallel-memory-manual-test.js +197 -0
  212. package/_site/_test/parallel-memory-scope-test.js +228 -0
  213. package/_site/_test/parallel-memory-source-test.js +227 -0
  214. package/_site/_test/parallel-memory-sync-test.js +238 -0
  215. package/_site/_test/parallel-memory-view-r8-reload-gate-test.js +123 -0
  216. package/_site/_test/parallel-nav-failure-test.js +119 -0
  217. package/_site/_test/parallel-nav-isolation-test.js +308 -0
  218. package/_site/_test/parallel-nav-isolation-vector-path-test.js +90 -0
  219. package/_site/_test/parallel-nav-offline-test.js +272 -0
  220. package/_site/_test/parallel-nav-operability-test.js +257 -0
  221. package/_site/_test/parallel-nav-switch-test.js +247 -0
  222. package/_site/_test/parallel-nav-theme-test.js +130 -0
  223. package/_site/_test/parallel-nav-viewport-test.js +310 -0
  224. package/_site/_test/parallel-qa-engine-endpoint-guard-test.js +273 -0
  225. package/_site/_test/parallel-retrieval-followup-citation-test.js +219 -0
  226. package/_site/_test/parallel-retrieval-followup-corpus-test.js +287 -0
  227. package/_site/_test/parallel-workbench-independent-host.js +262 -0
  228. package/_site/_test/parallel-workbench-integration-5025-abort-probe.js +340 -0
  229. package/_site/_test/parallel-workbench-integration-5025-drafts-probe.js +588 -0
  230. package/_site/_test/parallel-workbench-integration-5025-env-check.js +317 -0
  231. package/_site/_test/parallel-workbench-integration-5025-model-probe.js +342 -0
  232. package/_site/_test/parallel-workbench-integration-5025-ordering-probe.js +326 -0
  233. package/_site/_test/parallel-workbench-model-probe.js +336 -0
  234. package/_site/_test/parallel-workbench-offline-test.js +169 -0
  235. package/_site/_test/parallel-workbench-provenance-trace.js +134 -0
  236. package/_site/_test/parallel-workbench-source-match.js +206 -0
  237. package/_site/_test/parallel-workbench-standalone-turn.js +349 -0
  238. package/_site/_test/path-consistency-test.js +151 -151
  239. package/_site/_test/pending-sweep-test.js +9 -9
  240. package/_site/_test/post-commit-automation-test.js +87 -87
  241. package/_site/_test/pr-consumer-contract-test.js +55 -55
  242. package/_site/_test/product-brand-contract-test.js +25 -25
  243. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  244. package/_site/_test/product-empty-state-test.js +148 -0
  245. package/_site/_test/product-import-ui-test.js +219 -104
  246. package/_site/_test/product-navigation-test.js +152 -0
  247. package/_site/_test/product-progress-status-test.js +84 -84
  248. package/_site/_test/project-delete-recovery-test.js +64 -64
  249. package/_site/_test/project-domain-create-test.js +91 -0
  250. package/_site/_test/project-domain-scope-retrieval-test.js +214 -0
  251. package/_site/_test/project-git-init-confirm-test.js +115 -0
  252. package/_site/_test/project-goal-editor-test.js +144 -144
  253. package/_site/_test/project-import-duplicate-alias-test.js +134 -0
  254. package/_site/_test/project-import-unicode-restart-test.js +140 -0
  255. package/_site/_test/project-lifecycle-transaction-test.js +106 -106
  256. package/_site/_test/project-module-partial-recovery-test.js +222 -0
  257. package/_site/_test/project-remove-running-guard-test.js +79 -79
  258. package/_site/_test/project-store-test.js +90 -90
  259. package/_site/_test/project-unbind-late-read-ui-test.js +250 -0
  260. package/_site/_test/project-unbind-writer-guard-test.js +231 -0
  261. package/_site/_test/prompt-settings-test.js +115 -115
  262. package/_site/_test/protected-architecture-gate-test.js +122 -122
  263. package/_site/_test/refactor-characterization-test.js +36 -36
  264. package/_site/_test/registry-events-test.js +38 -38
  265. package/_site/_test/release-version-sync-test.js +115 -115
  266. package/_site/_test/requirement-binding-test.js +113 -113
  267. package/_site/_test/requirement-recorder-test.js +173 -173
  268. package/_site/_test/run-all-tests.js +173 -161
  269. package/_site/_test/runtime-endpoint-test.js +64 -64
  270. package/_site/_test/scanner-test.js +83 -83
  271. package/_site/_test/server-security-test.js +96 -96
  272. package/_site/_test/sessions-stream-test.js +93 -93
  273. package/_site/_test/shared-contracts-test.js +58 -58
  274. package/_site/_test/shared-scope-root-binding-test.js +74 -67
  275. package/_site/_test/simple-import-test.js +98 -98
  276. package/_site/_test/startup-analysis-disabled-test.js +16 -16
  277. package/_site/_test/storage-foundation-test.js +78 -78
  278. package/_site/_test/structured-logger-test.js +101 -101
  279. package/_site/_test/t07b-cln-005-coverage-test.js +212 -0
  280. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  281. package/_site/_test/team-knowledge-store-test.js +132 -132
  282. package/_site/_test/test-server-cleanup-test.js +35 -35
  283. package/_site/_test/test-server-module-isolation-test.js +37 -37
  284. package/_site/_test/tracking-start-test.js +144 -144
  285. package/_site/_test/vector-storage-root-test.js +75 -75
  286. package/_site/_test/version-visibility-browser-probe.js +249 -0
  287. package/_site/_test/version-visibility-test.js +92 -0
  288. package/_site/_test/workbench-context-receipts-test.js +22 -22
  289. package/_site/_test/workbench-permission-test.js +117 -117
  290. package/_site/_test/zcode-capture-integration-test.js +112 -112
  291. package/_site/lib/ai-profile-resolver.js +78 -78
  292. package/_site/lib/ai-vendor-presets.js +83 -83
  293. package/_site/lib/ai-workspace.js +104 -104
  294. package/_site/lib/atomic-file.js +204 -204
  295. package/_site/lib/automation-config.js +140 -140
  296. package/_site/lib/bridge-adapter.js +260 -260
  297. package/_site/lib/bridge-consumer-service.js +402 -402
  298. package/_site/lib/capture-hook-config.js +108 -108
  299. package/_site/lib/claude-cli-runner.js +1599 -1599
  300. package/_site/lib/cli-instances.js +43 -43
  301. package/_site/lib/codex-context-status.js +17 -17
  302. package/_site/lib/codex-notify-config.js +193 -193
  303. package/_site/lib/commit-conversation-binder.js +247 -247
  304. package/_site/lib/commit-processing-ledger.js +27 -27
  305. package/_site/lib/commit-prompt.js +202 -202
  306. package/_site/lib/commit-reconciler.js +444 -444
  307. package/_site/lib/context-pack-builder.js +299 -299
  308. package/_site/lib/contracts.js +291 -291
  309. package/_site/lib/conversation-exclusions.js +204 -204
  310. package/_site/lib/conversation-query-service.js +161 -161
  311. package/_site/lib/conversation-store.js +387 -387
  312. package/_site/lib/data-dir.js +13 -13
  313. package/_site/lib/embedding-service.js +85 -85
  314. package/_site/lib/engine-connection.js +93 -93
  315. package/_site/lib/engine-knowledge-client.js +76 -72
  316. package/_site/lib/evidence-bundle.js +228 -228
  317. package/_site/lib/folder-picker-output.js +24 -24
  318. package/_site/lib/git-path.js +14 -14
  319. package/_site/lib/git-runner.js +62 -62
  320. package/_site/lib/github-team-store.js +1175 -1175
  321. package/_site/lib/hook-manager.js +203 -203
  322. package/_site/lib/index-builder.js +157 -157
  323. package/_site/lib/index-service.js +173 -173
  324. package/_site/lib/integration-auto-repair.js +77 -77
  325. package/_site/lib/integration-installer.js +1240 -1240
  326. package/_site/lib/integration-intent.js +88 -88
  327. package/_site/lib/integration-status-scan.js +99 -99
  328. package/_site/lib/integration-status-worker.js +14 -14
  329. package/_site/lib/kb-framework.js +191 -191
  330. package/_site/lib/kb-validator.js +124 -124
  331. package/_site/lib/knowledge-db.js +363 -363
  332. package/_site/lib/knowledge-promotion.js +435 -435
  333. package/_site/lib/knowledge-retrieval-service.js +401 -401
  334. package/_site/lib/knowledge-schema.js +121 -121
  335. package/_site/lib/knowledge-tool-runtime.js +366 -366
  336. package/_site/lib/live-gateway.js +78 -78
  337. package/_site/lib/llm-client.js +173 -173
  338. package/_site/lib/markdown-knowledge-indexer.js +230 -230
  339. package/_site/lib/model-context-windows.js +82 -82
  340. package/_site/lib/module-bridge.js +874 -874
  341. package/_site/lib/module-port-resolver.js +57 -57
  342. package/_site/lib/post-commit-automation.js +141 -141
  343. package/_site/lib/product-data.js +35 -35
  344. package/_site/lib/project-lifecycle-service.js +554 -554
  345. package/_site/lib/project-registry-store.js +129 -129
  346. package/_site/lib/project-store.js +268 -268
  347. package/_site/lib/prompt-registry.js +148 -148
  348. package/_site/lib/public-version-info.js +37 -0
  349. package/_site/lib/registry-events.js +16 -16
  350. package/_site/lib/requirement-adapters.js +10 -10
  351. package/_site/lib/requirement-binder.js +112 -112
  352. package/_site/lib/requirement-recorder.js +278 -278
  353. package/_site/lib/runtime-control.js +89 -89
  354. package/_site/lib/runtime-endpoint.js +160 -160
  355. package/_site/lib/scanner.js +298 -298
  356. package/_site/lib/server-app.js +1672 -1664
  357. package/_site/lib/settings-store.js +153 -153
  358. package/_site/lib/storage-layout.js +166 -166
  359. package/_site/lib/structured-logger.js +574 -574
  360. package/_site/lib/zcode-context-integration.js +60 -60
  361. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  362. package/_site/scripts/folder-picker.ps1 +156 -156
  363. package/_site/scripts/hook-trigger.js +158 -158
  364. package/_site/scripts/install-module-candidates.js +69 -69
  365. package/_site/scripts/pack-module-candidates.js +89 -89
  366. package/_site/scripts/sync-product-content.js +28 -28
  367. package/_site/scripts/sync-release-version.js +164 -164
  368. package/_site/scripts/vendor-modules.js +49 -49
  369. package/_site/scripts/verify-module-runtime.js +50 -50
  370. package/_site/scripts/verify-product-runtime.js +36 -36
  371. package/_site/scripts/verify-retrieval-gold.js +83 -83
  372. package/_site/server.js +15 -15
  373. package/_site/start.bat +26 -26
  374. package/_site/stop.bat +11 -11
  375. package/bin/project-knowledge-integrations.js +254 -254
  376. package/bin/project-knowledge-kb.js +67 -67
  377. package/bin/project-knowledge-mcp.js +201 -201
  378. package/bin/project-knowledge.js +269 -170
  379. package/claude-prompts.json +21 -21
  380. package/docs/README.zh-CN.md +207 -205
  381. package/docs/assets/architecture.svg +181 -181
  382. package/docs/assets/logo.svg +49 -49
  383. package/docs/fresh-v5-start.md +44 -44
  384. package/docs/leader-glm-workbench-playbook.md +122 -122
  385. package/docs/project-registry-schema.md +33 -33
  386. package/docs/release-5.0.16-acceptance.md +176 -176
  387. package/docs/release-5.0.17-acceptance.md +117 -117
  388. package/docs/release-5.0.19-acceptance.md +39 -39
  389. package/docs/team-knowledge-mode-a-plan.md +73 -73
  390. package/docs/testing-strategy.md +33 -33
  391. package/module-runtime-manifest.json +66 -63
  392. package/package.json +113 -110
  393. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  394. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  395. package/plugins/project-knowledge/.mcp.json +1 -1
  396. package/templates/change.md +30 -30
  397. package/templates/project-readme.md +52 -52
  398. package/ui/index.html +3 -2
  399. package/ui/version-info.css +44 -0
  400. package/ui/version-info.js +24 -0
  401. package/vendor-manifest.json +55 -55
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+
3
+ // P-ENGINE-REVIEW-R2 ENG-007 boundary re-check: what exactly does 启动对账 cover?
4
+ //
5
+ // Contract verified against the current docs and the real implementation
6
+ // BEFORE this driver was written:
7
+ // * README.md (触发面): "消费 event-bridge journal 的 commit boundary 与关联
8
+ // 会话事件 -> CommitReconciler 对账/认领" - the reconciliation source is the
9
+ // JOURNAL, not Git history.
10
+ // * README.md:119: "未启动服务时,Hook 已捕获的事件保留于 journal,服务消费后
11
+ // 才生成分析任务" - retention/compensation scope is events the hook captured.
12
+ // * src/coordinator.js consumeJournal(): the only compensation path reads
13
+ // journal events from cursor+1; there is no git-log scan anywhere.
14
+ // * src/runtime/commit-reconciler.js:173: "failed SHA is never retried,
15
+ // scanned, or inferred from history."
16
+ // => The startup reconciliation contract is JOURNAL RECOVERY. A commit whose
17
+ // hook never wrote a boundary event is outside that contract (uncovered
18
+ // boundary, honestly stated - not papered over as "compensated").
19
+ //
20
+ // This driver pins BOTH SHAs of the offline window and answers the Leader's
21
+ // question directly:
22
+ // commit A - broken hook (no boundary event) -> NOT found back by any Git
23
+ // reconcile (no such mechanism exists).
24
+ // commit B - working hook (boundary event) -> compensated by the startup
25
+ // journal drain (the ONLY recovery path).
26
+ // Then, decisively: a REAL boundary event for A is delivered after the fact
27
+ // with the real bridge CLI. The event is consumed, but B's success moved
28
+ // lastAnalyzedCommit to shaB, so the already-analysed-ancestor guard skips A
29
+ // (A is B's parent in real Git history): A is skipped over by B even when
30
+ // its evidence is handed to the engine explicitly. A is not analysed, no run
31
+ // is created - the guarantee "已完成SHA不重跑" extends to ancestors of the
32
+ // analysed head.
33
+ //
34
+ // Not repeated here (already green in engine-final/eng007, untouched): the
35
+ // duplicate-B replay and the restart-no-rerun saga.
36
+
37
+ const assert = require('node:assert/strict');
38
+ const fs = require('node:fs');
39
+ const path = require('node:path');
40
+ const { spawnSync } = require('node:child_process');
41
+ const {
42
+ makeLaneDir, buildFixtureRepo, installPostCommitHook, startBridge, startVector,
43
+ startEngine, waitUntil, git, gitRun, readJsonl, saveEvidence, isolatedEnv, BRIDGE_CLI,
44
+ } = require('./helpers/parallel-engine-final-harness.js');
45
+ const { createStandinAnalyzer } = require('./helpers/parallel-engine-final-standin-analyzer.js');
46
+
47
+ const ARTIFACTS = process.env.PK_TEST_EVIDENCE_ROOT
48
+ ? path.join(process.env.PK_TEST_EVIDENCE_ROOT, 'eng007reconcile')
49
+ : path.resolve(__dirname, '../../qa-autopilot/artifacts/parallel-20261023/engine-review-r3/reruns/eng007reconcile');
50
+ const PROJECT_ID = 'project-eng007r2';
51
+
52
+ function findJournalFiles(root) {
53
+ const out = [];
54
+ const walk = dir => {
55
+ if (!fs.existsSync(dir)) return;
56
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
57
+ const absolute = path.join(dir, entry.name);
58
+ if (entry.isDirectory()) walk(absolute);
59
+ else if (entry.name === 'events.jsonl') out.push(absolute);
60
+ }
61
+ };
62
+ walk(root);
63
+ return out;
64
+ }
65
+ function boundaryEvents(...roots) {
66
+ const events = [];
67
+ for (const file of roots.flatMap(root => findJournalFiles(root))) {
68
+ for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
69
+ const event = JSON.parse(line);
70
+ if (event.schema === 'git-commit-boundary/v1') events.push(event);
71
+ }
72
+ }
73
+ return events;
74
+ }
75
+
76
+ async function main() {
77
+ const lane = makeLaneDir('eng007r2');
78
+ const commands = [];
79
+ const evidence = {
80
+ caseId: 'ENG-007 (R2 boundary re-check: startup reconciliation contract scope, both SHAs)',
81
+ laneDir: lane,
82
+ contract: {
83
+ source: 'README.md 触发面 + README.md:119 + src/coordinator.js consumeJournal (journal events are the only compensation source; no git-log scan) + src/runtime/commit-reconciler.js:173 (failed SHA never retried/scanned/inferred from history)',
84
+ scope: 'startup reconciliation = JOURNAL RECOVERY only; commits whose hook never wrote a boundary event are NOT covered (uncovered boundary, by design and by docs)',
85
+ },
86
+ phases: {},
87
+ commands,
88
+ };
89
+
90
+ const fixture = buildFixtureRepo(lane, 'eng007r2', []);
91
+ const knowledgePath = path.join(lane, 'knowledge-eng007r2');
92
+ const storePath = path.join(knowledgePath, 'dev-conversations');
93
+ fs.mkdirSync(storePath, { recursive: true });
94
+ const home = path.join(lane, 'bridge-home');
95
+ const dataDir = path.join(lane, 'engine-data');
96
+ const vectorRoot = path.join(lane, 'vector-root');
97
+ const eventsFile = path.join(lane, 'analyzer-events.jsonl');
98
+
99
+ const bridge = startBridge(home);
100
+ let engine = null;
101
+ let vector = null;
102
+ const analyzer = createStandinAnalyzer({ eventsFile });
103
+ const invocationsFor = sha => readJsonl(eventsFile).filter(entry => entry.kind === 'start' && entry.commitSha === sha).length;
104
+
105
+ try {
106
+ const bridgeUrl = await bridge.listen();
107
+ vector = await startVector(vectorRoot);
108
+ const registered = await bridge.projectRegistry.addProject({ home, path: fixture.repoPath, store: storePath });
109
+ const { repoIdentity } = await bridge.resolveRepoContext(fixture.repoPath);
110
+
111
+ // Register the project (consumer off), then stop the engine - the offline window begins.
112
+ engine = await startEngine({ dataDir, bridgeUrl, vectorUrl: vector.url, analyzer, consume: false });
113
+ await engine.listen();
114
+ engine.runtime.store.addProject({
115
+ projectId: PROJECT_ID, repoPath: fixture.repoPath, knowledgePath,
116
+ trackingStartCommit: fixture.baseline, repoIdentity, journalId: registered.id, conversationBaselineCursor: 0,
117
+ });
118
+ await engine.close();
119
+ engine = null;
120
+ commands.push('engine lifecycle 0: project registered (consumer off), engine stopped - offline window begins');
121
+
122
+ // ----- commit A: BROKEN hook (engine down, no boundary event) -----
123
+ installPostCommitHook(fixture.repoPath, { home, mode: 'broken' });
124
+ fs.writeFileSync(path.join(fixture.repoPath, 'offline-a.txt'), `offline feature A\n${'a'.repeat(64)}\n`);
125
+ git(fixture.repoPath, ['add', '.']);
126
+ const commitA = gitRun(fixture.repoPath, ['commit', '-qm', 'offline feature A']);
127
+ assert.equal(commitA.code, 0, `Git must not be blocked by the failing hook (exit ${commitA.code})`);
128
+ const shaA = git(fixture.repoPath, ['rev-parse', 'HEAD']);
129
+ assert.match(commitA.stderr, /commit-boundary\.missing\.js/, 'the hook really failed');
130
+
131
+ // ----- commit B: WORKING hook (engine down, boundary event on disk) -----
132
+ installPostCommitHook(fixture.repoPath, { home, mode: 'ok' });
133
+ fs.writeFileSync(path.join(fixture.repoPath, 'offline-b.txt'), `offline feature B\n${'b'.repeat(64)}\n`);
134
+ git(fixture.repoPath, ['add', '.']);
135
+ const commitB = gitRun(fixture.repoPath, ['commit', '-qm', 'offline feature B']);
136
+ assert.equal(commitB.code, 0);
137
+ const shaB = git(fixture.repoPath, ['rev-parse', 'HEAD']);
138
+ const boundariesOffline = boundaryEvents(home, storePath);
139
+ assert.equal(boundariesOffline.filter(e => e.commitSha === shaB).length, 1, 'B wrote exactly one boundary event while the engine was down');
140
+ assert.equal(boundariesOffline.filter(e => e.commitSha === shaA).length, 0, 'A wrote no boundary event');
141
+
142
+ // A really is B's parent in real Git history (the "skipped over by B" precondition).
143
+ const aIsAncestorOfB = gitRun(fixture.repoPath, ['merge-base', '--is-ancestor', shaA, shaB]);
144
+ assert.equal(aIsAncestorOfB.code, 0, 'A must be an ancestor of B in the fixture history');
145
+
146
+ // ----- restart: startup drain compensates B only -----
147
+ engine = await startEngine({ dataDir, bridgeUrl, vectorUrl: vector.url, analyzer, consume: true });
148
+ await engine.listen();
149
+ const runB = await waitUntil(async () => {
150
+ const run = engine.runtime.store.listRuns(PROJECT_ID).find(r => r.commitSha === shaB);
151
+ return run && run.state === 'succeeded' ? run : null;
152
+ }, 60000, 'startup drain compensates B');
153
+ await new Promise(resolve => setTimeout(resolve, 2000)); // let the drain settle fully before judging A
154
+ const runsAfterStart = engine.runtime.store.listRuns(PROJECT_ID);
155
+ const ledgerA = path.join(dataDir, 'runtime', 'processed-commits', PROJECT_ID, `${shaA}.json`);
156
+ const ledgerB = path.join(dataDir, 'runtime', 'processed-commits', PROJECT_ID, `${shaB}.json`);
157
+ const lastAnalyzed = engine.runtime.store.getProject(PROJECT_ID)?.runtime?.lastAnalyzedCommit || null;
158
+ evidence.phases.offlineWindow = {
159
+ commitA: { sha: shaA, subject: 'offline feature A', hook: 'broken (missing CLI)', gitExit: commitA.code, boundaryEvents: 0 },
160
+ commitB: { sha: shaB, subject: 'offline feature B', hook: 'working (real bridge CLI)', gitExit: commitB.code, boundaryEvents: 1 },
161
+ aIsAncestorOfB: aIsAncestorOfB.code === 0,
162
+ startupCompensation: {
163
+ runForB: { runId: runB.runId, trigger: runB.trigger, state: runB.state, indexState: runB.indexState },
164
+ runsForA: runsAfterStart.filter(r => r.commitSha === shaA).length,
165
+ totalRuns: runsAfterStart.length,
166
+ analyzerInvocations: { forA: invocationsFor(shaA), forB: invocationsFor(shaB) },
167
+ ledgerAExists: fs.existsSync(ledgerA),
168
+ ledgerBStatus: fs.existsSync(ledgerB) ? JSON.parse(fs.readFileSync(ledgerB, 'utf8')).status : null,
169
+ lastAnalyzedCommit: lastAnalyzed,
170
+ },
171
+ };
172
+ assert.equal(runB.trigger, 'git-hook');
173
+ assert.equal(invocationsFor(shaB), 1, 'B compensated exactly once');
174
+ assert.equal(runsAfterStart.filter(r => r.commitSha === shaA).length, 0, 'A has no run: the startup drain found nothing for A (no boundary event, no Git-history scan)');
175
+ assert.equal(invocationsFor(shaA), 0, 'A was never analysed');
176
+ assert.equal(fs.existsSync(ledgerA), false, 'A is not in the processing ledger');
177
+ assert.equal(lastAnalyzed, shaB, 'B\'s success moved lastAnalyzedCommit to shaB');
178
+ commands.push(`restart -> drain compensated B (${shaB.slice(0, 8)}) only; A (${shaA.slice(0, 8)}) has no run/ledger/analysis`);
179
+
180
+ // ----- decisive check: hand A's boundary event to the engine AFTER the fact -----
181
+ const cursorBefore = engine.runtime.store.state.journalCursors[registered.id] || 0;
182
+ const replayA = spawnSync(process.execPath, [BRIDGE_CLI, '--home', home, '--cwd', fixture.repoPath, '--sha', shaA], {
183
+ cwd: fixture.repoPath, windowsHide: true, encoding: 'utf8', env: isolatedEnv(),
184
+ });
185
+ assert.equal(replayA.status, 0, `replaying A's boundary event failed: ${replayA.stderr}`);
186
+ await waitUntil(async () => (engine.runtime.store.state.journalCursors[registered.id] || 0) > cursorBefore, 20000, 'A\'s after-the-fact boundary event is consumed');
187
+ await new Promise(resolve => setTimeout(resolve, 2000));
188
+ const runsAfterReplayA = engine.runtime.store.listRuns(PROJECT_ID);
189
+ evidence.phases.afterTheFactBoundaryForA = {
190
+ replay: { ok: JSON.parse(replayA.stdout).ok, commitSha: shaA },
191
+ journalCursorBefore: cursorBefore,
192
+ journalCursorAfter: engine.runtime.store.state.journalCursors[registered.id] || 0,
193
+ runsForA: runsAfterReplayA.filter(r => r.commitSha === shaA).length,
194
+ totalRuns: runsAfterReplayA.length,
195
+ analyzerInvocationsForA: invocationsFor(shaA),
196
+ ledgerAExists: fs.existsSync(ledgerA),
197
+ verdict: 'A\'s evidence IS consumed (cursor advanced, boundary projection written) but the already-analysed-ancestor guard (baseline=lastAnalyzedCommit=shaB) skips A: no run, no analysis. A is skipped over by B; it is not "found back" by any Git reconcile - and none exists in the engine.',
198
+ };
199
+ assert.equal(runsAfterReplayA.filter(r => r.commitSha === shaA).length, 0, 'even with its boundary event delivered after the fact, A must not be re-analysed (ancestor guard)');
200
+ assert.equal(invocationsFor(shaA), 0, 'the analyzer was never invoked for A');
201
+ assert.equal(runsAfterReplayA.length, 1, 'the run set stays at exactly B\'s run');
202
+ commands.push(`A's boundary event delivered after the fact -> consumed (cursor ${cursorBefore}->${evidence.phases.afterTheFactBoundaryForA.journalCursorAfter}), ancestor guard skipped A (no run, no analysis)`);
203
+
204
+ evidence.verdict = {
205
+ 'question A是否被Git reconcile找回': 'NO - the engine has no Git-history reconciliation at all; the only compensation source is the bridge journal (README 触发面 + README:119, src/coordinator.js consumeJournal, src/runtime/commit-reconciler.js:173)',
206
+ 'question A是否被B越过': 'YES - B\'s promotion moved lastAnalyzedCommit to shaB; even when A\'s boundary event was delivered after the fact with the real CLI, the already-analysed-ancestor guard skipped A (consumed, projected, never enqueued)',
207
+ 'contract scope (honest)': '启动对账 = journal 恢复:只有成功写入 boundary 事件的提交会被补回。无 boundary 事件的提交(hook 故障)不在契约内,本 driver 将其作为未覆盖边界显式记录,不计入"已补偿"',
208
+ 'expected1 Git不被Hook故障阻塞': 'PASS (commit A exit 0 with a failing hook; commit exists)',
209
+ 'expected2 启动对账补漏': 'PASS within the journal-recovery contract (B compensated via startup drain; A uncovered by design - stated, not claimed)',
210
+ 'expected3 已完成SHA不重跑': 'PASS (B analysed once; ledger + ancestor guards held; A skipped as analysed-ancestor)',
211
+ boundary: 'unchanged paths (duplicate-B replay, restart-no-rerun) stay covered by engine-final/eng007 first-round evidence; they were not re-run here',
212
+ analyzerStandin: 'deterministic stand-in - NO real model output covered; vector-hub real with deterministic test embeddings - paid embedding API NOT covered',
213
+ };
214
+ await engine.close();
215
+ await vector.close();
216
+ await bridge.close();
217
+ saveEvidence(ARTIFACTS, 'eng007-reconcile-evidence.json', evidence);
218
+ console.log('ENG-007 R2 reconcile re-check PASS');
219
+ process.exit(0);
220
+ } catch (error) {
221
+ evidence.verdict = { failed: String((error && error.stack) || error) };
222
+ try { await engine?.close(); } catch {}
223
+ try { await vector?.close(); } catch {}
224
+ try { await bridge.close(); } catch {}
225
+ saveEvidence(ARTIFACTS, 'eng007-reconcile-evidence.json', evidence);
226
+ console.error(`ENG-007 R2 reconcile re-check FAIL: ${error.message}`);
227
+ process.exit(1);
228
+ }
229
+ }
230
+
231
+ main();
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ // P-ENG-ADVANCED ENG-009 晋升中断恢复 (promotion interruption recovery) acceptance driver.
4
+ //
5
+ // Case (qa-autopilot/config/test-cases.json ENG-009, P0):
6
+ // steps: 在许可的故障注入点中断测试writer,再重启。
7
+ // expected: 1) journal恢复一致。 2) 不留下半晋升的假成功。 3) pointer顺序正确。
8
+ //
9
+ // Layer A (server-level real crash): the vendored engine child is HARD-KILLED
10
+ // (uncatchable SIGKILL on POSIX, TerminateProcess on Windows - no graceful
11
+ // cleanup) while a real run is mid-analysis, then a second engine child starts
12
+ // on the same data directory - exercising the real single-writer stale-lock
13
+ // break, initialize() recoverAll(), run recovery (running -> queued/recovering),
14
+ // and claim retry semantics.
15
+ //
16
+ // Layer B (fault-point crashes): the vendored KnowledgePromotionService's own
17
+ // production `fault` constructor hook hard-exits a child process at three
18
+ // permitted phases (journal-prepared / operation-applied:0 / promotion-verified,
19
+ // journal already persisted as awaiting-state-advance), then a fresh process
20
+ // runs the same recoverAll() the coordinator runs at every startup.
21
+ //
22
+ // Analyzer = declarative stand-in (NO real model output). Vector endpoint =
23
+ // loopback double.
24
+
25
+ const assert = require('node:assert/strict');
26
+ const { spawn } = require('node:child_process');
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+ const crypto = require('node:crypto');
30
+ const {
31
+ makeLaneDir, buildFixtureRepo, waitUntil, callJson, startVectorDouble,
32
+ startEngineChild, readEventsFile, writePlan, saveEvidence, isolatedEnv,
33
+ } = require('./helpers/parallel-engine-advanced-harness.js');
34
+
35
+ const ARTIFACTS = process.env.PK_TEST_EVIDENCE_ROOT
36
+ ? path.join(process.env.PK_TEST_EVIDENCE_ROOT, 'eng009')
37
+ : path.resolve(__dirname, '../../qa-autopilot/artifacts/parallel-20261023/engine-review-r3/reruns/eng009');
38
+ const FAULT_WRITER = path.join(__dirname, 'helpers', 'parallel-engine-advanced-fault-writer.js');
39
+ const PROJECT_ID = 'project-eng009';
40
+
41
+ function runFaultWriter(mode, workDir) {
42
+ return new Promise((resolve, reject) => {
43
+ const proc = spawn(process.execPath, [FAULT_WRITER, mode, workDir], { windowsHide: true, env: isolatedEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
44
+ let stderr = '';
45
+ proc.stderr.on('data', chunk => { stderr += chunk; });
46
+ proc.on('error', reject);
47
+ proc.on('close', code => resolve({ mode, code, stderr: stderr.slice(0, 500) }));
48
+ });
49
+ }
50
+
51
+ function hashFile(file) {
52
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16);
53
+ }
54
+
55
+ async function main() {
56
+ const lane = makeLaneDir('eng009');
57
+ const commands = [];
58
+ const evidence = { caseId: 'ENG-009', laneDir: lane, phases: {}, commands };
59
+
60
+ // ---------- Layer A: hard crash of a live engine child, then restart ----------
61
+ const fixture = buildFixtureRepo(lane, 'eng009', ['X']);
62
+ const knowledgePath = path.join(lane, 'knowledge-eng009');
63
+ fs.mkdirSync(knowledgePath, { recursive: true });
64
+ const planFile = path.join(lane, 'analyzer-plan.json');
65
+ const eventsFile = path.join(lane, 'analyzer-events.jsonl');
66
+ writePlan(planFile, { default: {}, rules: [{ projectId: PROJECT_ID, commitSha: fixture.shas.X, delayMs: 5000 }] });
67
+
68
+ const vector = await startVectorDouble();
69
+ const dataDir = path.join(lane, 'engine-data');
70
+ const engine1 = await startEngineChild({ label: 'eng009', dataDir, vectorUrl: vector.url, planFile, eventsFile });
71
+ commands.push('LayerA: POST /api/runs X (analyzer stall 5s)', 'LayerA: hard-kill engine child mid-analysis', 'LayerA: restart engine child on same dataDir');
72
+
73
+ try {
74
+ assert.equal((await callJson(engine1.base, '/api/projects', {
75
+ projectId: PROJECT_ID, name: 'eng009', repoPath: fixture.repoPath, knowledgePath, trackingStartCommit: fixture.baseline,
76
+ })).status, 200);
77
+ const runX = (await callJson(engine1.base, '/api/runs', { projectId: PROJECT_ID, commitSha: fixture.shas.X })).body.run;
78
+ await waitUntil(() => readEventsFile(eventsFile).some(e => e.kind === 'start' && e.commitSha === fixture.shas.X), 20000, 'analyzer started X');
79
+
80
+ const killCode = await engine1.killHard();
81
+ const stateRaw = JSON.parse(fs.readFileSync(path.join(dataDir, 'engine-state.json'), 'utf8'));
82
+ const crashedRun = stateRaw.runs.find(r => r.runId === runX.runId);
83
+ const lockFile = path.join(dataDir, 'engine-runtime.lock');
84
+ const lockOwner = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
85
+ const promotionsDir = path.join(dataDir, 'runtime', 'promotions', PROJECT_ID);
86
+ evidence.phases.layerACrash = {
87
+ killExitCode: killCode,
88
+ killSignal: engine1.proc.signalCode,
89
+ killWasUncatchable: engine1.proc.signalCode === 'SIGKILL' || process.platform === 'win32',
90
+ runStateOnDiskAfterKill: { state: crashedRun.state, phase: crashedRun.phase },
91
+ lockOwnerPid: lockOwner.pid,
92
+ lockOwnerPidWasEngineChild: lockOwner.pid === engine1.proc.pid,
93
+ promotionJournalsOnDisk: fs.existsSync(promotionsDir) ? fs.readdirSync(promotionsDir) : [],
94
+ pointerAtCrash: stateRaw.projects.find(p => p.projectId === PROJECT_ID).runtime.lastAnalyzedCommit,
95
+ };
96
+ assert.equal(crashedRun.state, 'running', 'run was live when the writer was interrupted');
97
+ assert.equal(lockOwner.pid, engine1.proc.pid, 'single-writer lock belonged to the killed engine');
98
+ assert.equal(evidence.phases.layerACrash.pointerAtCrash, fixture.baseline, 'pointer untouched before durable promotion');
99
+
100
+ // Restart on the same dataDir: stale lock must break via dead owner pid.
101
+ const engine2 = await startEngineChild({ label: 'eng009-restart', dataDir, vectorUrl: vector.url, planFile, eventsFile });
102
+ try {
103
+ const recovered = await waitUntil(async () => {
104
+ const runs = (await callJson(engine2.base, `/api/runs?projectId=${PROJECT_ID}`)).body.runs;
105
+ return runs.length === 1 && runs[0].state === 'succeeded' ? runs[0] : null;
106
+ }, 45000, 'restarted engine completes the interrupted run');
107
+ const events = readEventsFile(eventsFile);
108
+ const starts = events.filter(e => e.kind === 'start' && e.commitSha === fixture.shas.X);
109
+ const runtime = (await callJson(engine2.base, `/api/snapshot?projectId=${PROJECT_ID}`)).body.projects.find(p => p.projectId === PROJECT_ID).runtime;
110
+ const knowledgeFiles = fs.readdirSync(path.join(knowledgePath, 'changes'));
111
+ const journalFiles = fs.readdirSync(promotionsDir);
112
+ const journal = JSON.parse(fs.readFileSync(path.join(promotionsDir, journalFiles[0]), 'utf8'));
113
+ evidence.phases.layerARecovery = {
114
+ runId: recovered.runId,
115
+ sameRunIdAsBeforeCrash: recovered.runId === runX.runId,
116
+ state: recovered.state,
117
+ promotedAt: recovered.promotedAt,
118
+ analyzerStartsForX: starts.length,
119
+ attemptEvidence: 'crash before durable state advance -> engine re-runs analysis (attempt 2) rather than faking success',
120
+ lastAnalyzedCommit: runtime.lastAnalyzedCommit,
121
+ knowledgeChangesFiles: knowledgeFiles,
122
+ journalPhase: journal.phase,
123
+ journalOperationsApplied: journal.operations.map(op => ({ path: op.path, applied: op.applied })),
124
+ lockFileRecreatedByRestart: JSON.parse(fs.readFileSync(lockFile, 'utf8')).pid === engine2.proc.pid,
125
+ };
126
+ assert.equal(recovered.runId, runX.runId, 'no duplicate run created by recovery');
127
+ assert.equal(starts.length, 2, 'analyzer ran once pre-crash and once after restart (no fake success)');
128
+ assert.equal(runtime.lastAnalyzedCommit, fixture.shas.X, 'pointer advanced exactly to X after recovery');
129
+ assert.deepEqual(knowledgeFiles, [`${fixture.shas.X.slice(0, 12)}.md`], 'exactly one promoted knowledge file');
130
+ assert.equal(journal.phase, 'completed', 'promotion journal reaches terminal completed phase');
131
+ assert.ok(journal.operations.every(op => op.applied), 'all journal operations applied');
132
+ await engine2.stop();
133
+ } catch (error) {
134
+ try { await engine2.stop(); } catch {}
135
+ throw error;
136
+ }
137
+
138
+ // ---------- Layer B: fault-point crashes through the promotion service's own hook ----------
139
+ const scenarios = [
140
+ { mode: 'crash-prepared', expect: 'rolled-back' },
141
+ { mode: 'crash-mid-apply', expect: 'rolled-back' },
142
+ { mode: 'crash-after-apply', expect: 'completed' },
143
+ ];
144
+ evidence.phases.layerB = {};
145
+ for (const scenario of scenarios) {
146
+ const workDir = path.join(lane, `fault-${scenario.mode}`);
147
+ const crash = await runFaultWriter(scenario.mode, workDir);
148
+ commands.push(`LayerB ${scenario.mode}: fault-writer exit ${crash.code}`);
149
+ assert.equal(crash.code, 137, `${scenario.mode} child hard-exited at the fault point (got ${crash.code}, stderr: ${crash.stderr})`);
150
+ const recover = await runFaultWriter('recover', workDir);
151
+ assert.equal(recover.code, 0, `${scenario.mode} recovery process succeeded (${recover.stderr})`);
152
+ const result = JSON.parse(fs.readFileSync(path.join(workDir, 'fault-result.json'), 'utf8'));
153
+ const readme = fs.readFileSync(path.join(workDir, 'knowledge', 'README.md'), 'utf8');
154
+ const changesDir = path.join(workDir, 'knowledge', 'changes');
155
+ const recoverAgain = await runFaultWriter('recover', workDir);
156
+ const againResult = JSON.parse(fs.readFileSync(path.join(workDir, 'fault-result.json'), 'utf8'));
157
+ evidence.phases.layerB[scenario.mode] = {
158
+ crashExitCode: crash.code,
159
+ recoverAll: result.recoverAll,
160
+ lastAnalyzedCommit: result.lastAnalyzedCommit,
161
+ activeClaimRunId: result.activeClaimRunId,
162
+ readmeStartsWith: readme.slice(0, 40),
163
+ changesFiles: result.changesFiles,
164
+ indexDirty: result.indexDirty,
165
+ indexGeneration: result.indexGeneration,
166
+ secondRecoverAll: againResult.recoverAll,
167
+ };
168
+ if (scenario.expect === 'rolled-back') {
169
+ assert.equal(result.recoverAll[0].phase, 'rolled-back', `${scenario.mode}: journal rolled back consistently`);
170
+ assert.ok(readme.startsWith('# eng009 fixture knowledge (original)'), `${scenario.mode}: knowledge restored - no half-promoted state`);
171
+ assert.deepEqual(result.changesFiles, [], `${scenario.mode}: created file removed by rollback`);
172
+ assert.equal(result.lastAnalyzedCommit, null, `${scenario.mode}: pointer NOT advanced`);
173
+ assert.equal(result.activeClaimRunId, 'run-11111111-2222-3333-4444-555555555555', `${scenario.mode}: claim retained for explicit retry, not silently faked`);
174
+ } else {
175
+ assert.equal(result.recoverAll[0].phase, 'completed', 'after-apply: journal completed forward');
176
+ assert.ok(readme.startsWith('# eng009 fixture knowledge (replaced)'), 'after-apply: both operations final');
177
+ assert.equal(result.changesFiles.length, 1, 'after-apply: created file present');
178
+ assert.equal(result.lastAnalyzedCommit, 'a'.repeat(40), 'after-apply: pointer advanced exactly once via recovery');
179
+ assert.equal(result.activeClaimRunId, null, 'after-apply: active claim cleared');
180
+ assert.equal(result.indexDirty, true, 'after-apply: index marked dirty for repair');
181
+ assert.equal(result.indexGeneration, 1, 'after-apply: index generation advanced once');
182
+ }
183
+ assert.ok(againResult.recoverAll.every(r => r.skipped === true || r.phase === 'rolled-back' || r.phase === 'completed'), 'second recovery is idempotent (skipped)');
184
+ }
185
+
186
+ evidence.verdict = {
187
+ 'expected1 journal恢复一致(终端相位+二次恢复幂等)': 'PASS',
188
+ 'expected2 不留下半晋升的假成功(mid-apply回滚/after-apply完整推进/崩溃后重分析而非假成功)': 'PASS',
189
+ 'expected3 pointer顺序正确(仅经journal完成推进, 基线→X单一序列)': 'PASS',
190
+ injectionPoints: 'Layer A: real process hard-kill mid-analysis; Layer B: vendored KnowledgePromotionService production fault hook (journal-prepared / operation-applied:0 / promotion-verified)',
191
+ analyzerStandin: 'declarative stand-in - NO real model output covered',
192
+ vectorDouble: 'loopback HTTP double - vector-hub itself not covered',
193
+ };
194
+ await vector.close();
195
+ saveEvidence(ARTIFACTS, 'eng009-evidence.json', evidence);
196
+ console.log('ENG-009 PASS');
197
+ process.exitCode = 0;
198
+ } catch (error) {
199
+ evidence.verdict = { failed: String(error && error.stack || error) };
200
+ try { await engine1.stop(); } catch {}
201
+ try { await vector.close(); } catch {}
202
+ saveEvidence(ARTIFACTS, 'eng009-evidence.json', evidence);
203
+ console.error(`ENG-009 FAIL: ${error.message}`);
204
+ process.exitCode = 1;
205
+ }
206
+ }
207
+
208
+ main();
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ // P-ENG-ADVANCED ENG-010 索引独立失败 (independent index failure) acceptance driver.
4
+ //
5
+ // Case (qa-autopilot/config/test-cases.json ENG-010, P1):
6
+ // steps: Markdown晋升成功后令向量服务失败。
7
+ // expected: 1) 知识已更新/索引待修复分开。
8
+ // 2) 不回滚Markdown、不重复付费分析。
9
+ //
10
+ // Real behavior under test: vendored knowledge-engine child process, real
11
+ // promotion to the project knowledge tree on disk, then the vector endpoint
12
+ // (loopback double scripted to HTTP 503 EMBEDDING_UNAVAILABLE) failing exactly
13
+ // at index time. Verified via coordinator.index() semantics (src/coordinator.js):
14
+ // promotedAt is persisted BEFORE index(); an index failure moves the run to
15
+ // state 'index-pending' with error INDEX_FAILED “知识已更新,索引待修复。”
16
+ // while the Markdown promotion stays on disk; retry()/restart re-enter
17
+ // execute()'s promotedAt fast-path (line 219) which only re-indexes.
18
+ //
19
+ // Analyzer = declarative stand-in (NO real model output). Vector endpoint =
20
+ // loopback double (vector-hub itself not covered).
21
+
22
+ const assert = require('node:assert/strict');
23
+ const crypto = require('node:crypto');
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const {
27
+ makeLaneDir, buildFixtureRepo, waitUntil, callJson, startVectorDouble,
28
+ startEngineChild, readEventsFile, writePlan, saveEvidence,
29
+ } = require('./helpers/parallel-engine-advanced-harness.js');
30
+
31
+ const ARTIFACTS = process.env.PK_TEST_EVIDENCE_ROOT
32
+ ? path.join(process.env.PK_TEST_EVIDENCE_ROOT, 'eng010')
33
+ : path.resolve(__dirname, '../../qa-autopilot/artifacts/parallel-20261023/engine-review-r3/reruns/eng010');
34
+ const PROJECT_ID = 'project-eng010';
35
+ const VECTOR_FAILURE = { kind: 'fail', httpStatus: 503, code: 'EMBEDDING_UNAVAILABLE', message: 'embedding backend unavailable (scripted)' };
36
+
37
+ function hashFile(file) {
38
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16);
39
+ }
40
+
41
+ function readRunFromDisk(dataDir, runId) {
42
+ const state = JSON.parse(fs.readFileSync(path.join(dataDir, 'engine-state.json'), 'utf8'));
43
+ return { state, run: state.runs.find(r => r.runId === runId) };
44
+ }
45
+
46
+ async function main() {
47
+ const lane = makeLaneDir('eng010');
48
+ const commands = [];
49
+ const evidence = { caseId: 'ENG-010', laneDir: lane, phases: {}, commands };
50
+ const fixture = buildFixtureRepo(lane, 'eng010', ['X', 'Y']);
51
+ const knowledgePath = path.join(lane, 'knowledge-eng010');
52
+ fs.mkdirSync(knowledgePath, { recursive: true });
53
+ const planFile = path.join(lane, 'analyzer-plan.json');
54
+ const eventsFile = path.join(lane, 'analyzer-events.jsonl');
55
+ writePlan(planFile, { default: { delayMs: 300 }, rules: [] });
56
+
57
+ const vector = await startVectorDouble();
58
+ vector.setMode(VECTOR_FAILURE);
59
+ const dataDir = path.join(lane, 'engine-data');
60
+ const engine1 = await startEngineChild({ label: 'eng010', dataDir, vectorUrl: vector.url, planFile, eventsFile });
61
+ commands.push('POST /api/runs X while vector double scripted 503 EMBEDDING_UNAVAILABLE', 'POST /api/runs/X retry after vector double restored', 'stop engine, re-fail vector, POST /api/runs Y -> index-pending', 'restart engine with vector restored (initialize() auto-retry)');
62
+
63
+ const changesDir = path.join(knowledgePath, 'changes');
64
+ const fileFor = sha => path.join(changesDir, `${sha.slice(0, 12)}.md`);
65
+ const startsFor = sha => readEventsFile(eventsFile).filter(e => e.kind === 'start' && e.commitSha === sha).length;
66
+ const runById = async (engine, runId) => (await callJson(engine.base, `/api/runs?projectId=${PROJECT_ID}`)).body.runs.find(r => r.runId === runId);
67
+ const runtimeOf = async engine => (await callJson(engine.base, `/api/snapshot?projectId=${PROJECT_ID}`)).body.projects.find(p => p.projectId === PROJECT_ID).runtime;
68
+
69
+ try {
70
+ assert.equal((await callJson(engine1.base, '/api/projects', {
71
+ projectId: PROJECT_ID, name: 'eng010', repoPath: fixture.repoPath, knowledgePath, trackingStartCommit: fixture.baseline,
72
+ })).status, 200);
73
+ const runX = (await callJson(engine1.base, '/api/runs', { projectId: PROJECT_ID, commitSha: fixture.shas.X })).body.run;
74
+
75
+ // --- Expected 1: 知识已更新 / 索引待修复 分开 ---
76
+ const stuck = await waitUntil(async () => {
77
+ const run = await runById(engine1, runX.runId);
78
+ return run?.state === 'index-pending' ? run : null;
79
+ }, 30000, 'run reaches index-pending (promotion done, index failed)');
80
+ const runtime1 = await runtimeOf(engine1);
81
+ const fileX = fileFor(fixture.shas.X);
82
+ const hashX = hashFile(fileX);
83
+ const diskX = readRunFromDisk(dataDir, runX.runId);
84
+ evidence.phases.indexFailure = {
85
+ run: { state: stuck.state, phase: stuck.phase, indexState: stuck.indexState, promotedAt: stuck.promotedAt, error: stuck.error },
86
+ runtime: { lastAnalyzedCommit: runtime1.lastAnalyzedCommit, indexDirty: runtime1.index.dirty, indexStatus: runtime1.index.status },
87
+ knowledgeFile: { path: path.basename(fileX), bytes: fs.statSync(fileX).size, hash: hashX },
88
+ analyzerStartsForX: startsFor(fixture.shas.X),
89
+ vectorSyncRequests: vector.requests.filter(r => r.url === '/api/sync').length,
90
+ diskIndexAttempts: diskX.run.indexAttempts,
91
+ diskState: diskX.run.state,
92
+ };
93
+ assert.equal(stuck.state, 'index-pending', 'index failure is a SEPARATE state, not failed');
94
+ assert.equal(stuck.phase, 'index-pending');
95
+ assert.equal(stuck.indexState, 'failed');
96
+ assert.ok(stuck.promotedAt, 'promotion timestamp recorded - knowledge IS updated');
97
+ assert.equal(stuck.error.code, 'INDEX_FAILED');
98
+ assert.equal(stuck.error.message, '知识已更新,索引待修复。');
99
+ assert.equal(stuck.error.retryable, true);
100
+ assert.equal(stuck.error.details.failureKind, 'HTTP_ERROR');
101
+ assert.equal(stuck.error.details.httpStatus, 503);
102
+ assert.equal(stuck.error.details.upstreamCode, 'EMBEDDING_UNAVAILABLE');
103
+ assert.equal(stuck.error.details.attempt, 1);
104
+ assert.equal(runtime1.lastAnalyzedCommit, fixture.shas.X, 'pointer advanced - Markdown promotion committed');
105
+ assert.equal(runtime1.index.dirty, true, 'runtime index marked dirty while knowledge is current');
106
+ assert.ok(fs.existsSync(fileX), 'promoted Markdown exists on disk');
107
+ assert.ok(fs.statSync(fileX).size > 0, 'promoted Markdown has real bytes');
108
+ assert.equal(startsFor(fixture.shas.X), 1, 'analyzer ran exactly once');
109
+ assert.equal(diskX.run.indexAttempts, 1, 'disk state records exactly one index attempt');
110
+
111
+ // --- Expected 2 (part 1): explicit retry repairs the index only ---
112
+ const promotedAtX = stuck.promotedAt;
113
+ vector.setMode({ kind: 'ok' });
114
+ const retried = await callJson(engine1.base, `/api/runs/${encodeURIComponent(runX.runId)}/retry`, {});
115
+ assert.equal(retried.status, 200, `retry failed: ${JSON.stringify(retried.body)}`);
116
+ const healed = await waitUntil(async () => {
117
+ const run = await runById(engine1, runX.runId);
118
+ return run?.state === 'succeeded' ? run : null;
119
+ }, 30000, 'retried run succeeds after vector restored');
120
+ const runtime2 = await runtimeOf(engine1);
121
+ const diskX2 = readRunFromDisk(dataDir, runX.runId);
122
+ evidence.phases.explicitRetry = {
123
+ run: { state: healed.state, phase: healed.phase, indexState: healed.indexState, promotedAt: healed.promotedAt, error: healed.error },
124
+ promotedAtUnchanged: healed.promotedAt === promotedAtX,
125
+ knowledgeFileHashAfterRetry: hashFile(fileX),
126
+ knowledgeFileHashBefore: hashX,
127
+ analyzerStartsForX: startsFor(fixture.shas.X),
128
+ diskIndexAttempts: diskX2.run.indexAttempts,
129
+ vectorSyncRequests: vector.requests.filter(r => r.url === '/api/sync').length,
130
+ runtime: { indexDirty: runtime2.index.dirty, indexStatus: runtime2.index.status },
131
+ };
132
+ assert.equal(healed.phase, 'ready');
133
+ assert.equal(healed.indexState, 'succeeded');
134
+ assert.equal(healed.error, null);
135
+ assert.equal(healed.promotedAt, promotedAtX, 'promotion timestamp untouched by index retry');
136
+ assert.equal(hashFile(fileX), hashX, 'Markdown bytes NOT rolled back or rewritten by index retry');
137
+ assert.equal(startsFor(fixture.shas.X), 1, 'no second analyzer run - no duplicate paid analysis');
138
+ assert.equal(diskX2.run.indexAttempts, 2, 'second index attempt recorded on disk');
139
+ assert.equal(vector.requests.filter(r => r.url === '/api/sync').length, 2, 'one failed + one successful sync request');
140
+ assert.equal(runtime2.index.dirty, false, 'index repaired: dirty cleared');
141
+ assert.equal(runtime2.index.status, 'ready');
142
+
143
+ // --- Expected 2 (part 2): restart auto-repairs index-pending without re-analysis ---
144
+ vector.setMode(VECTOR_FAILURE);
145
+ const runY = (await callJson(engine1.base, '/api/runs', { projectId: PROJECT_ID, commitSha: fixture.shas.Y })).body.run;
146
+ const stuckY = await waitUntil(async () => {
147
+ const run = await runById(engine1, runY.runId);
148
+ return run?.state === 'index-pending' ? run : null;
149
+ }, 30000, 'second run also reaches index-pending');
150
+ const promotedAtY = stuckY.promotedAt;
151
+ const fileY = fileFor(fixture.shas.Y);
152
+ const hashY = hashFile(fileY);
153
+ const startsYBeforeRestart = startsFor(fixture.shas.Y);
154
+ await engine1.stop();
155
+ const diskY = readRunFromDisk(dataDir, runY.runId);
156
+ evidence.phases.restartSetup = {
157
+ runY: { state: diskY.run.state, phase: diskY.run.phase, promotedAt: diskY.run.promotedAt, diskIndexAttempts: diskY.run.indexAttempts },
158
+ knowledgeFileY: { hash: hashY, analyzerStarts: startsYBeforeRestart },
159
+ };
160
+ assert.equal(diskY.run.state, 'index-pending', 'index-pending persisted across shutdown');
161
+
162
+ vector.setMode({ kind: 'ok' });
163
+ const engine2 = await startEngineChild({ label: 'eng010-restart', dataDir, vectorUrl: vector.url, planFile, eventsFile });
164
+ try {
165
+ const autoHealed = await waitUntil(async () => {
166
+ const run = await runById(engine2, runY.runId);
167
+ return run?.state === 'succeeded' ? run : null;
168
+ }, 45000, 'restart auto-retries index-pending run at initialize()');
169
+ const diskY2 = readRunFromDisk(dataDir, runY.runId);
170
+ const runtime3 = await runtimeOf(engine2);
171
+ evidence.phases.restartAutoRetry = {
172
+ runY: { state: autoHealed.state, phase: autoHealed.phase, indexState: autoHealed.indexState, promotedAt: autoHealed.promotedAt, error: autoHealed.error },
173
+ promotedAtUnchanged: autoHealed.promotedAt === promotedAtY,
174
+ knowledgeFileYHashAfterRestart: hashFile(fileY),
175
+ knowledgeFileYHashBefore: hashY,
176
+ analyzerStartsForYTotal: startsFor(fixture.shas.Y),
177
+ diskIndexAttempts: diskY2.run.indexAttempts,
178
+ runtime: { indexDirty: runtime3.index.dirty, indexStatus: runtime3.index.status },
179
+ syncsForY: vector.requests.filter(r => r.url === '/api/sync' && r.body.project === PROJECT_ID).length,
180
+ };
181
+ assert.equal(autoHealed.promotedAt, promotedAtY, 'restart did not re-promote');
182
+ assert.equal(hashFile(fileY), hashY, 'Markdown bytes unchanged across restart auto-repair');
183
+ assert.equal(startsFor(fixture.shas.Y), startsYBeforeRestart, 'restart auto-repair did NOT run the analyzer again');
184
+ assert.equal(diskY2.run.indexAttempts, 2, 'restart performed exactly one more index attempt');
185
+ assert.equal(runtime3.index.dirty, false, 'index clean after auto-repair');
186
+ await engine2.stop();
187
+ } catch (error) {
188
+ try { await engine2.stop(); } catch {}
189
+ throw error;
190
+ }
191
+
192
+ evidence.verdict = {
193
+ 'expected1 知识已更新/索引待修复分开(index-pending独立状态+INDEX_FAILED可重试+指针已推进+dirty标记)': 'PASS',
194
+ 'expected2 不回滚Markdown、不重复付费分析(字节hash不变/analyzer恰好1次/promotedAt不变/重试只补索引)': 'PASS',
195
+ extraRestartAutoRetry: 'PASS - initialize() automatically retries persisted index-pending without re-analysis',
196
+ analyzerStandin: 'declarative stand-in - NO real model output covered',
197
+ vectorDouble: 'loopback HTTP double scripted 503 EMBEDDING_UNAVAILABLE - vector-hub itself not covered',
198
+ };
199
+ await vector.close();
200
+ saveEvidence(ARTIFACTS, 'eng010-evidence.json', evidence);
201
+ console.log('ENG-010 PASS');
202
+ process.exitCode = 0;
203
+ } catch (error) {
204
+ evidence.verdict = { failed: String(error && error.stack || error) };
205
+ try { await engine1.stop(); } catch {}
206
+ try { await vector.close(); } catch {}
207
+ saveEvidence(ARTIFACTS, 'eng010-evidence.json', evidence);
208
+ console.error(`ENG-010 FAIL: ${error.message}`);
209
+ process.exitCode = 1;
210
+ }
211
+ }
212
+
213
+ main();