@sanqianx/project-knowledge 5.0.24 → 5.0.26

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 +68 -65
  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,250 @@
1
+ 'use strict';
2
+ // PRJ-010: a late registry read must not resurrect an unbound project.
3
+ //
4
+ // The steps of the case are "hold the registry read, complete the unbind, then
5
+ // let the held read answer". The expectations are:
6
+ // 1. the old answer must not bring the unbound project back;
7
+ // 2. a later refresh still shows the project as unbound;
8
+ // 3. the other project is untouched.
9
+ //
10
+ // The instance is an isolated product server with its own data directory, its
11
+ // own knowledge root and its own module URLs. The browser runs in a new
12
+ // Chrome profile. The product is reached through a pass-through proxy that
13
+ // can hold exactly one response, so the late read is a real HTTP answer that
14
+ // arrives after the unbind — the proxy forwards bodies and responses verbatim
15
+ // and only decides when the answer is delivered.
16
+ //
17
+ // The knowledge engine is the real vendored module; the three other modules
18
+ // are local fixtures that answer 503, so only the engine participates.
19
+
20
+ const assert = require('node:assert/strict');
21
+ const crypto = require('node:crypto');
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+ const { spawnSync } = require('node:child_process');
25
+ const { spawnServer } = require('./helpers/spawn-server');
26
+ const { findChrome } = require('./helpers/find-chrome');
27
+ const { launchCdpBrowser, waitFor } = require('./helpers/cdp-browser');
28
+ const { attachNetworkCapture } = require('./helpers/cdp-network-capture');
29
+ const { createModuleFixture, createUpstreamProxy, createProductProxy } = require('./helpers/module-fixture');
30
+ const freePort = require('./helpers/free-port');
31
+ const { applyDriverIsolation } = require('./helpers/parallel-qa-engine-endpoint-isolate');
32
+ const { guardedCreateEngineServer } = require('./helpers/parallel-qa-engine-endpoint-guard');
33
+
34
+ const ROOT = path.resolve(__dirname, '../..');
35
+
36
+ function git(cwd, args) {
37
+ const result = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
38
+ assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
39
+ return String(result.stdout || '').trim();
40
+ }
41
+
42
+ function makeRepo(root, name) {
43
+ const repo = path.join(root, name);
44
+ fs.mkdirSync(repo, { recursive: true });
45
+ git(repo, ['init', '-q', '-b', 'main']);
46
+ git(repo, ['config', 'user.email', 'late-read@local.invalid']);
47
+ git(repo, ['config', 'user.name', 'Late Read Regression']);
48
+ fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`);
49
+ git(repo, ['add', '.']); git(repo, ['commit', '-qm', 'baseline']);
50
+ return repo;
51
+ }
52
+
53
+ async function main() {
54
+ // Driver isolation FIRST (Chrome captured before the wipe), before any
55
+ // product module is required.
56
+ const applied = applyDriverIsolation({ prefix: 'pk-qa-prj-late-read', captureChrome: true });
57
+ const fixture = fs.mkdtempSync(path.join(applied.layout.temp, 'pk-prj-late-read-'));
58
+ const dataDir = path.join(fixture, 'data');
59
+ const knowledgeRoot = path.join(fixture, 'knowledge');
60
+ const repoA = makeRepo(fixture, 'repo-unbound');
61
+ const repoB = makeRepo(fixture, 'repo-untouched');
62
+ const knowledgeA = path.join(knowledgeRoot, '迟到读项目');
63
+ const knowledgeB = path.join(knowledgeRoot, '无关项目');
64
+ fs.mkdirSync(knowledgeA, { recursive: true }); fs.mkdirSync(knowledgeB, { recursive: true });
65
+
66
+ const engineToken = crypto.randomBytes(24).toString('hex');
67
+ // OWN fixtures first, then the engine constructed against them: the
68
+ // coordinator ignores env and would otherwise fall back to LIVE 8787/8790.
69
+ const terminal = createModuleFixture({ kind: 'terminal' });
70
+ const vectorHub = createModuleFixture({ kind: 'vectorHub' });
71
+ const eventBridge = createModuleFixture({ kind: 'eventBridge' });
72
+ for (const fixtureService of [terminal, vectorHub, eventBridge]) {
73
+ await fixtureService.start();
74
+ fixtureService.setOnline(false);
75
+ }
76
+ const engine = guardedCreateEngineServer({ dataDir: path.join(fixture, 'engine'), runtimeToken: engineToken, debug: true, consume: false, vectorUrl: vectorHub.url, bridgeUrl: eventBridge.url });
77
+ await engine.ready;
78
+ await new Promise(resolve => engine.server.listen(0, '127.0.0.1', resolve));
79
+ const engineUrl = `http://127.0.0.1:${engine.server.address().port}`;
80
+ const engineProxy = createUpstreamProxy({ target: engineUrl, token: engineToken, tag: 'engine' });
81
+ await engineProxy.start();
82
+
83
+ const port = await freePort();
84
+ const base = `http://127.0.0.1:${port}`;
85
+ const server = spawnServer({
86
+ root: ROOT,
87
+ port,
88
+ dataDir,
89
+ tag: 'project-late-read',
90
+ analysisOwner: 'engine',
91
+ extraEnv: {
92
+ KB_MODULES_REGISTER: '1',
93
+ KB_MODULES_AUTOSTART: '0',
94
+ KB_KNOWLEDGE_ENGINE_URL: engineProxy.url,
95
+ KB_TERMINAL_URL: terminal.url,
96
+ KB_VECTORHUB_URL: vectorHub.url,
97
+ KB_EVENTBRIDGE_URL: eventBridge.url,
98
+ },
99
+ });
100
+
101
+ // The browser talks to the product through this proxy: it can hold one
102
+ // answer and release it later, which is the late read of the case.
103
+ const productProxy = createProductProxy({ target: base, tag: 'product' });
104
+ await productProxy.start();
105
+
106
+ let browser;
107
+ const api = async (method, route, body) => {
108
+ const response = await fetch(base + route, {
109
+ method,
110
+ headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
111
+ body: body === undefined ? undefined : JSON.stringify(body),
112
+ signal: AbortSignal.timeout(20000),
113
+ });
114
+ return { status: response.status, body: await response.json() };
115
+ };
116
+ const engineApi = async (method, route, body) => {
117
+ const response = await fetch(engineUrl + route, {
118
+ method,
119
+ headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
120
+ body: body === undefined ? undefined : JSON.stringify(body),
121
+ signal: AbortSignal.timeout(20000),
122
+ });
123
+ return { status: response.status, body: await response.json() };
124
+ };
125
+
126
+ try {
127
+ await waitFor(async () => {
128
+ try { return (await api('GET', '/api/health')).status === 200; } catch { return false; }
129
+ }, 'the isolated product instance', 20000);
130
+ assert.equal((await api('PATCH', '/api/settings', { knowledge: { rootPath: knowledgeRoot } })).status, 200);
131
+
132
+ const importedA = await api('POST', '/api/projects/import', { localPath: repoA, knowledgePath: knowledgeA, displayName: '迟到读项目' });
133
+ assert.equal(importedA.status, 201, JSON.stringify(importedA.body));
134
+ const projectA = importedA.body.projectId;
135
+ const importedB = await api('POST', '/api/projects/import', { localPath: repoB, knowledgePath: knowledgeB, displayName: '无关项目' });
136
+ assert.equal(importedB.status, 201, JSON.stringify(importedB.body));
137
+ const projectB = importedB.body.projectId;
138
+ assert.ok(projectA && projectB && projectA !== projectB);
139
+
140
+ browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(fixture, 'browser'), url: productProxy.url, width: 1440, height: 900 });
141
+ await browser.setViewport(1440, 900);
142
+ const capture = attachNetworkCapture(browser);
143
+ await capture.enable();
144
+ const run = expression => browser.evaluate(expression);
145
+ const click = selector => run(`document.querySelector(${JSON.stringify(selector)}).click()`);
146
+ const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
147
+
148
+ await waitFor(() => run("document.getElementById('homeContent').textContent.includes('迟到读项目') && document.getElementById('homeContent').textContent.includes('无关项目')"), 'both projects in the browser');
149
+ await click('[data-view="projects"]');
150
+ await waitFor(() => run(`document.getElementById('projectTree').textContent.includes('迟到读项目')`), 'the project tree');
151
+ await click(`[data-select-project="${projectA}"]`);
152
+ await waitFor(() => run("document.getElementById('projectDetail').textContent.includes('迟到读项目')"), 'the project detail');
153
+ assert.ok(await run("document.getElementById('projectDetail').querySelector('[data-action=\"unbind\"]') !== null"), 'the unbound project offers the unbind action');
154
+
155
+ // Open the unbind dialog, then hold the next registry read. The held
156
+ // answer is the one the browser started before the DELETE.
157
+ await click('[data-action="unbind"]');
158
+ await waitFor(() => run("document.getElementById('unbindDialog').open === true"), 'the unbind dialog');
159
+ const projectionReads = () => capture.find(entry => entry.method === 'GET' && entry.url.includes('/api/projects/projection'));
160
+ const readsBefore = projectionReads().length;
161
+ productProxy.hold(entry => entry.method === 'GET' && entry.path.startsWith('/api/projects/projection'), { once: true });
162
+ // The registry read that follows the unbind fails on purpose: then the
163
+ // late answer is the only successful read after the unbind, and the UI
164
+ // state cannot be silently cleaned up by a later read.
165
+ productProxy.fail(entry => entry.method === 'GET' && entry.path.startsWith('/api/projects/projection'), 503, '注入的注册读取故障', { once: true });
166
+
167
+ const heldDeadline = Date.now() + 8000;
168
+ while (productProxy.heldCount() === 0 && Date.now() < heldDeadline) {
169
+ await run("document.dispatchEvent(new Event('visibilitychange'))");
170
+ await wait(100);
171
+ }
172
+ assert.equal(productProxy.heldCount(), 1, 'exactly one registry read must be held');
173
+ const heldRequest = productProxy.state.requests.find(entry => entry.method === 'GET' && entry.path.startsWith('/api/projects/projection'));
174
+ assert.ok(heldRequest, 'the held registry read is recorded by the proxy');
175
+ const heldAt = heldRequest.at;
176
+
177
+ // The unbind itself runs in the browser, against the real product.
178
+ await click('#confirmUnbind');
179
+ const deleteRequest = await waitFor(() => productProxy.state.requests.find(entry => entry.method === 'DELETE' && entry.path.includes(`/api/projects/${encodeURIComponent(projectA)}`)) || null, 'the unbind request', 15000);
180
+ // The unbind ends with a forced registry refresh. That refresh opens a
181
+ // new read epoch, so once it is under way the held read is old. The
182
+ // browser queues that read behind the held connection, so the epoch
183
+ // change is observed through the sibling read of the same refresh.
184
+ const laterRefresh = await waitFor(() => productProxy.state.requests.find(entry => entry.method === 'GET' && entry.path === '/api/brain' && entry.at > deleteRequest.at) || null, 'the registry refresh after the unbind', 15000);
185
+ assert.ok(heldAt < deleteRequest.at, 'the held read started before the unbind');
186
+ assert.ok(deleteRequest.at < laterRefresh.at, 'the unbind precedes the registry refresh that follows it');
187
+
188
+ // Now the old answer is delivered: it still lists the unbound project.
189
+ const heldPayload = JSON.parse(productProxy.state.held[0].payload.toString('utf8'));
190
+ assert.equal(heldPayload.projects.some(project => project.projectId === projectA), true, 'the held answer is the old list, it still contains the unbound project');
191
+ const released = productProxy.releaseHeld();
192
+ const releasedAt = Date.now();
193
+ assert.equal(released, 1, 'the held answer is delivered after the unbind');
194
+ assert.ok(releasedAt - heldAt < 3000, `the late answer must arrive inside the browser read timeout (gap ${releasedAt - heldAt}ms), otherwise the case proves nothing`);
195
+
196
+ await waitFor(() => run("document.getElementById('toast').textContent.includes('项目已从列表移除')"), 'the unbind to finish', 20000);
197
+
198
+ const heldCaptureEntry = await waitFor(() => projectionReads().length > readsBefore ? projectionReads()[readsBefore] : null, 'the held registry read in the browser', 5000);
199
+ await waitFor(() => heldCaptureEntry.status !== null, 'the late answer to reach the browser', 5000);
200
+ assert.equal(heldCaptureEntry.status, 200, 'the late answer is a real successful registry read');
201
+ await wait(500);
202
+
203
+ // 1. The old answer must not bring the unbound project back. Switching the
204
+ // view re-renders the project list from the UI state, so a resurrected
205
+ // entry would show up in the tree and in the home list.
206
+ await click('[data-view="home"]');
207
+ await waitFor(() => run("document.getElementById('homeContent').textContent.includes('无关项目')"), 'the home list');
208
+ assert.equal(await run("document.getElementById('homeContent').textContent.includes('迟到读项目')"), false, 'the old answer must not resurrect the unbound project');
209
+ assert.equal(await run("document.getElementById('homeContent').textContent.includes('无关项目')"), true, 'the other project stays listed');
210
+ await click('[data-view="projects"]');
211
+ await waitFor(() => run("document.getElementById('projectTree').textContent.includes('无关项目')"), 'the project tree after the late answer');
212
+ assert.equal(await run("document.getElementById('projectTree').textContent.includes('迟到读项目')"), false, 'the unbound project must not reappear in the tree');
213
+
214
+ // The product itself dropped the project, and the engine dropped it too.
215
+ const aggregated = (await api('GET', '/api/projects/aggregated')).body.projects;
216
+ assert.equal(aggregated.some(project => project.projectId === projectA), false, 'the product must not register the unbound project');
217
+ assert.equal(aggregated.some(project => project.projectId === projectB), true, 'the other project stays registered');
218
+
219
+ // 2. A later refresh still shows the project as unbound.
220
+ const before = capture.find(entry => entry.method === 'GET' && entry.url.includes('/api/projects/projection')).length;
221
+ await run("document.dispatchEvent(new Event('visibilitychange'))");
222
+ await waitFor(() => capture.find(entry => entry.method === 'GET' && entry.url.includes('/api/projects/projection')).length > before, 'the later refresh read', 10000);
223
+ await wait(600);
224
+ await click('[data-view="home"]');
225
+ await waitFor(() => run("document.getElementById('homeContent').textContent.includes('无关项目')"), 'the home list after the later refresh');
226
+ assert.equal(await run("document.getElementById('homeContent').textContent.includes('迟到读项目')"), false, 'the project stays unbound after a later refresh');
227
+
228
+ // 3. The other project is untouched: it is still registered in the product
229
+ // and in the real engine, and its files stayed on disk.
230
+ const engineProjects = (await engineApi('GET', '/api/projects')).body.projects;
231
+ assert.equal(engineProjects.some(project => project.projectId === projectA), false, 'the engine drops the unbound project');
232
+ assert.equal(engineProjects.some(project => project.projectId === projectB), true, 'the engine keeps the other project');
233
+ assert.ok(fs.existsSync(path.join(repoA, 'README.md')), 'the unbound project keeps its code');
234
+ assert.ok(fs.existsSync(knowledgeA), 'the unbound project keeps its knowledge directory');
235
+ assert.ok(fs.existsSync(path.join(repoB, 'README.md')), 'the other project keeps its code');
236
+ assert.equal(browser.exceptions.length, 0, 'no uncaught product JavaScript exceptions');
237
+
238
+ console.log('project-unbind-late-read-ui-test PASS (the held registry read arrives after the unbind and does not resurrect the project; the other project is untouched)');
239
+ } finally {
240
+ await browser?.close();
241
+ await server.cleanup();
242
+ await productProxy.close();
243
+ await engine.close().catch(() => {});
244
+ await engineProxy.close();
245
+ for (const fixtureService of [terminal, vectorHub, eventBridge]) await fixtureService.close();
246
+ fs.rmSync(fixture, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
247
+ }
248
+ }
249
+
250
+ main().catch(error => { console.error(error.stack || error.message); process.exitCode = 1; });
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+ // PRJ-009: unbinding is write-protected.
3
+ //
4
+ // The steps of the case are "prepare a queued task and a running write, then
5
+ // request the unbind for each". The expectations are:
6
+ // 1. a queued task is cancelled by the contract;
7
+ // 2. an active writer blocks the unbind;
8
+ // 3. the code, the knowledge files and the session history stay on disk.
9
+ //
10
+ // The knowledge engine is the real vendored module
11
+ // (_modules/knowledge-engine), started by this test on its own port with its
12
+ // own data directory. The product under test is an isolated instance that
13
+ // registers the fixture project into that engine; the three other modules are
14
+ // local fixtures that answer 503, so only the engine participates.
15
+ //
16
+ // Boundary: the engine runs without a model, so no analysis can be executed
17
+ // here. The "running writer" precondition is therefore written through the
18
+ // engine's own run store (the same field the engine itself checks before it
19
+ // removes a project). Everything else is the product's own behaviour.
20
+
21
+ const assert = require('node:assert/strict');
22
+ const crypto = require('node:crypto');
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const { spawnSync } = require('node:child_process');
26
+ const { spawnServer } = require('./helpers/spawn-server');
27
+ const { createModuleFixture, createUpstreamProxy } = require('./helpers/module-fixture');
28
+ const freePort = require('./helpers/free-port');
29
+ const { applyDriverIsolation } = require('./helpers/parallel-qa-engine-endpoint-isolate');
30
+ const { guardedCreateEngineServer } = require('./helpers/parallel-qa-engine-endpoint-guard');
31
+
32
+ const ROOT = path.resolve(__dirname, '../..');
33
+
34
+ function git(cwd, args) {
35
+ const result = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
36
+ assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
37
+ return String(result.stdout || '').trim();
38
+ }
39
+
40
+ function makeRepo(root, name) {
41
+ const repo = path.join(root, name);
42
+ fs.mkdirSync(repo, { recursive: true });
43
+ git(repo, ['init', '-q', '-b', 'main']);
44
+ git(repo, ['config', 'user.email', 'unbind@local.invalid']);
45
+ git(repo, ['config', 'user.name', 'Unbind Regression']);
46
+ fs.writeFileSync(path.join(repo, 'README.md'), `# ${name}\n`);
47
+ fs.writeFileSync(path.join(repo, 'code.js'), `export const value = '${name}';\n`);
48
+ git(repo, ['add', '.']); git(repo, ['commit', '-qm', 'baseline']);
49
+ return { repo, head: git(repo, ['rev-parse', 'HEAD']) };
50
+ }
51
+
52
+ function hashTree(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
+ if (entry.name === '.git') continue; // the Git directory is not project data
58
+ const absolute = path.join(dir, entry.name);
59
+ if (entry.isDirectory()) { walk(absolute); continue; }
60
+ out[path.relative(root, absolute).replace(/\\/g, '/')] = crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
61
+ }
62
+ };
63
+ walk(root);
64
+ return out;
65
+ }
66
+
67
+ async function waitForHealth(base, timeoutMs = 20000) {
68
+ const deadline = Date.now() + timeoutMs;
69
+ while (Date.now() < deadline) {
70
+ try {
71
+ const response = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(2000) });
72
+ if (response.ok) return;
73
+ } catch {}
74
+ await new Promise(resolve => setTimeout(resolve, 100));
75
+ }
76
+ throw new Error('Timed out waiting for the isolated server on ' + base);
77
+ }
78
+
79
+ async function main() {
80
+ // Driver isolation FIRST, before any product module is required.
81
+ const applied = applyDriverIsolation({ prefix: 'pk-qa-prj-unbind' });
82
+ const fixture = fs.mkdtempSync(path.join(applied.layout.temp, 'pk-prj-unbind-'));
83
+ const dataDir = path.join(fixture, 'data');
84
+ const knowledgeRoot = path.join(fixture, 'knowledge');
85
+ const first = makeRepo(fixture, 'repo-queued');
86
+ const second = makeRepo(fixture, 'repo-writing');
87
+ const firstKnowledge = path.join(knowledgeRoot, 'queued');
88
+ const secondKnowledge = path.join(knowledgeRoot, 'writing');
89
+ fs.mkdirSync(firstKnowledge, { recursive: true }); fs.mkdirSync(secondKnowledge, { recursive: true });
90
+
91
+ const engineToken = crypto.randomBytes(24).toString('hex');
92
+ // OWN fixtures first, then the engine constructed against them: the
93
+ // coordinator ignores env and would otherwise fall back to LIVE 8787/8790.
94
+ const terminal = createModuleFixture({ kind: 'terminal' });
95
+ const vectorHub = createModuleFixture({ kind: 'vectorHub' });
96
+ const eventBridge = createModuleFixture({ kind: 'eventBridge' });
97
+ for (const fixtureService of [terminal, vectorHub, eventBridge]) {
98
+ await fixtureService.start();
99
+ fixtureService.setOnline(false);
100
+ }
101
+ const engine = guardedCreateEngineServer({ dataDir: path.join(fixture, 'engine'), runtimeToken: engineToken, debug: true, consume: false, vectorUrl: vectorHub.url, bridgeUrl: eventBridge.url });
102
+ await engine.ready;
103
+ await new Promise(resolve => engine.server.listen(0, '127.0.0.1', resolve));
104
+ const engineUrl = `http://127.0.0.1:${engine.server.address().port}`;
105
+ const engineProxy = createUpstreamProxy({ target: engineUrl, token: engineToken, tag: 'engine' });
106
+ await engineProxy.start();
107
+
108
+ const port = await freePort();
109
+ const base = `http://127.0.0.1:${port}`;
110
+ const server = spawnServer({
111
+ root: ROOT,
112
+ port,
113
+ dataDir,
114
+ tag: 'project-unbind',
115
+ analysisOwner: 'engine',
116
+ extraEnv: {
117
+ KB_MODULES_REGISTER: '1',
118
+ KB_MODULES_AUTOSTART: '0',
119
+ KB_KNOWLEDGE_ENGINE_URL: engineProxy.url,
120
+ KB_TERMINAL_URL: terminal.url,
121
+ KB_VECTORHUB_URL: vectorHub.url,
122
+ KB_EVENTBRIDGE_URL: eventBridge.url,
123
+ },
124
+ });
125
+ const api = async (method, route, body) => {
126
+ const response = await fetch(base + route, {
127
+ method,
128
+ headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
129
+ body: body === undefined ? undefined : JSON.stringify(body),
130
+ signal: AbortSignal.timeout(20000),
131
+ });
132
+ return { status: response.status, body: await response.json() };
133
+ };
134
+ const engineApi = async (method, route, body) => {
135
+ const response = await fetch(engineUrl + route, {
136
+ method,
137
+ headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
138
+ body: body === undefined ? undefined : JSON.stringify(body),
139
+ signal: AbortSignal.timeout(20000),
140
+ });
141
+ return { status: response.status, body: await response.json() };
142
+ };
143
+
144
+ try {
145
+ await waitForHealth(base);
146
+ assert.equal((await api('PATCH', '/api/settings', { knowledge: { rootPath: knowledgeRoot } })).status, 200);
147
+
148
+ // ---- Case A: a queued maintenance task -------------------------------
149
+ const importedFirst = await api('POST', '/api/projects/import', { localPath: first.repo, knowledgePath: firstKnowledge, displayName: '排队任务项目' });
150
+ assert.equal(importedFirst.status, 201, JSON.stringify(importedFirst.body));
151
+ const firstProjectId = importedFirst.body.projectId;
152
+ const engineProjects = (await engineApi('GET', '/api/projects')).body.projects;
153
+ assert.ok(engineProjects.some(project => project.projectId === firstProjectId), 'the product must register the fixture project in the real engine');
154
+
155
+ const queued = await engineApi('POST', '/api/runs', { projectId: firstProjectId, commitSha: first.head });
156
+ assert.equal(queued.status, 200, JSON.stringify(queued.body));
157
+ const queuedRunId = queued.body.run.runId;
158
+ assert.equal(queued.body.run.state, 'queued', 'the prepared maintenance task must stay queued (not started)');
159
+
160
+ // The event-bridge keeps the session journal inside the knowledge
161
+ // directory. A controlled session record stands in for it here, because
162
+ // the journal consumer of the real event-bridge module is not part of this
163
+ // fixture.
164
+ fs.mkdirSync(path.join(firstKnowledge, 'dev-conversations'), { recursive: true });
165
+ fs.writeFileSync(path.join(firstKnowledge, 'dev-conversations', 'session-1.jsonl'), '{"role":"user","content":"会话记录"}\n');
166
+ const repoBefore = hashTree(first.repo);
167
+ const knowledgeBefore = hashTree(firstKnowledge);
168
+ const unbind = await api('DELETE', `/api/projects/${encodeURIComponent(firstProjectId)}`, { deleteKnowledge: false });
169
+ assert.equal(unbind.status, 200, JSON.stringify(unbind.body));
170
+
171
+ const runsAfter = (await engineApi('GET', `/api/runs?projectId=${encodeURIComponent(firstProjectId)}`)).body.runs;
172
+ const cancelled = runsAfter.find(run => run.runId === queuedRunId);
173
+ assert.ok(cancelled, 'the queued task must still be visible in the engine run history');
174
+ assert.equal(cancelled.state, 'cancelled', 'the queued task must be cancelled by the unbind');
175
+ assert.equal(cancelled.cancellationReason, 'project-unbound', 'the cancellation must record the contract reason');
176
+ assert.equal((await engineApi('GET', '/api/projects')).body.projects.some(project => project.projectId === firstProjectId), false, 'the engine must drop the unregistered project');
177
+ assert.equal((await api('GET', '/api/projects/aggregated')).body.projects.some(project => project.projectId === firstProjectId), false, 'the product must drop the unbound project');
178
+
179
+ // The disk keeps the code, the knowledge files and the session record.
180
+ const repoAfter = hashTree(first.repo);
181
+ const knowledgeAfter = hashTree(firstKnowledge);
182
+ assert.deepEqual(Object.keys(repoAfter), Object.keys(repoBefore), 'the code files must stay on disk');
183
+ assert.equal(repoAfter['README.md'], repoBefore['README.md'], 'the code file bytes must not change');
184
+ assert.equal(fs.existsSync(path.join(first.repo, '.git', 'hooks', 'post-commit')), false, 'the managed Hook is uninstalled by the unbind');
185
+ assert.equal(knowledgeAfter['dev-conversations/session-1.jsonl'], knowledgeBefore['dev-conversations/session-1.jsonl'], 'the session record must stay on disk');
186
+ assert.ok(fs.existsSync(firstKnowledge), 'the knowledge directory must survive the unbind');
187
+
188
+ // ---- Case B: an active writer blocks the unbind ----------------------
189
+ const importedSecond = await api('POST', '/api/projects/import', { localPath: second.repo, knowledgePath: secondKnowledge, displayName: '写入中项目' });
190
+ assert.equal(importedSecond.status, 201, JSON.stringify(importedSecond.body));
191
+ const secondProjectId = importedSecond.body.projectId;
192
+ const writing = await engineApi('POST', '/api/runs', { projectId: secondProjectId, commitSha: second.head });
193
+ assert.equal(writing.status, 200, JSON.stringify(writing.body));
194
+ const writingRunId = writing.body.run.runId;
195
+ // The engine cannot run a model-backed analysis in this fixture, so the
196
+ // running-writer precondition is written through the engine's own run
197
+ // store — the very state the engine checks before removing a project.
198
+ engine.store.updateRun(writingRunId, { state: 'running', phase: 'analyzing' });
199
+ assert.equal(engine.store.getRun(writingRunId).state, 'running');
200
+
201
+ const secondRepoBefore = hashTree(second.repo);
202
+ const secondKnowledgeBefore = hashTree(secondKnowledge);
203
+ const blocked = await api('DELETE', `/api/projects/${encodeURIComponent(secondProjectId)}`, { deleteKnowledge: false });
204
+ assert.equal(blocked.status, 409, JSON.stringify(blocked.body));
205
+ assert.equal(blocked.body.error.code, 'PROJECT_BUSY');
206
+ assert.equal(blocked.body.error.retryable, true, 'a blocked unbind must be reported as retryable');
207
+ assert.match(blocked.body.error.message, /写入结束/, 'the refusal must name the active write');
208
+ assert.ok((await api('GET', '/api/projects/aggregated')).body.projects.some(project => project.projectId === secondProjectId), 'a blocked unbind must keep the project registered');
209
+ assert.ok((await engineApi('GET', '/api/projects')).body.projects.some(project => project.projectId === secondProjectId), 'a blocked unbind must keep the engine registration');
210
+ assert.deepEqual(hashTree(second.repo), secondRepoBefore, 'a blocked unbind must not touch the code');
211
+ assert.deepEqual(hashTree(secondKnowledge), secondKnowledgeBefore, 'a blocked unbind must not touch the knowledge files');
212
+
213
+ // Once the write is over the same unbind succeeds.
214
+ engine.store.updateRun(writingRunId, { state: 'failed', phase: 'failed', finishedAt: new Date().toISOString() });
215
+ const retried = await api('DELETE', `/api/projects/${encodeURIComponent(secondProjectId)}`, { deleteKnowledge: false });
216
+ assert.equal(retried.status, 200, JSON.stringify(retried.body));
217
+ assert.equal((await api('GET', '/api/projects/aggregated')).body.projects.some(project => project.projectId === secondProjectId), false, 'the retried unbind removes the project');
218
+ assert.deepEqual(hashTree(second.repo), secondRepoBefore, 'the retried unbind must not touch the code');
219
+ assert.deepEqual(hashTree(secondKnowledge), secondKnowledgeBefore, 'the retried unbind must not touch the knowledge files');
220
+
221
+ console.log('project-unbind-writer-guard-test PASS (queued task cancelled, active writer blocks with a retryable 409, code/knowledge/session bytes preserved)');
222
+ } finally {
223
+ await server.cleanup();
224
+ await engine.close().catch(() => {});
225
+ await engineProxy.close();
226
+ for (const fixtureService of [terminal, vectorHub, eventBridge]) await fixtureService.close();
227
+ fs.rmSync(fixture, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
228
+ }
229
+ }
230
+
231
+ main().catch(error => { console.error(error.stack || error.message); process.exitCode = 1; });