@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.
- package/.agents/plugins/marketplace.json +20 -20
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +1361 -1353
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +349 -349
- package/_modules/knowledge-engine/package.json +1 -1
- package/_modules/knowledge-engine/src/coordinator.js +48 -3
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -140
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +238 -238
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -26
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +193 -193
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +394 -394
- package/_modules/knowledge-engine/src/runtime/contracts.js +282 -282
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +389 -389
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -228
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -62
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +505 -505
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +400 -400
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +171 -171
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -298
- package/_modules/knowledge-engine/ui/app.js +23 -2
- package/_site/README.md +30 -30
- package/_site/_test/agent-capture-mcp-ui-test.js +68 -68
- package/_site/_test/agent-capture-ui-test.js +101 -101
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/ai-profile-test.js +75 -75
- package/_site/_test/ai-vendor-presets-test.js +91 -91
- package/_site/_test/automatic-context-test.js +70 -70
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/bin-cli-test.js +230 -230
- package/_site/_test/bridge-adapter-test.js +95 -95
- package/_site/_test/bridge-consumer-service-test.js +310 -310
- package/_site/_test/candidate-bin-content-test.js +35 -35
- package/_site/_test/candidate-product-assets-test.js +13 -13
- package/_site/_test/candidate-release-contract-test.js +14 -14
- package/_site/_test/capture-hook-dedup-test.js +158 -158
- package/_site/_test/cdp-browser-failure-test.js +103 -103
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +90 -90
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
- package/_site/_test/claude-workbench-test.js +174 -174
- package/_site/_test/cli-instance-management-test.js +62 -62
- package/_site/_test/client-capture-restart-e2e-test.js +189 -189
- package/_site/_test/codex-conversation-projection-test.js +60 -60
- package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
- package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
- package/_site/_test/codex-notify-fanout-test.js +141 -134
- package/_site/_test/codex-notify-upgrade-test.js +253 -253
- package/_site/_test/commit-boundary-freeze-test.js +147 -147
- package/_site/_test/commit-conversation-binding-test.js +99 -99
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/configuration-generation-test.js +22 -22
- package/_site/_test/context-pack-test.js +200 -200
- package/_site/_test/conversation-api-test.js +119 -119
- package/_site/_test/conversation-origin-test.js +15 -15
- package/_site/_test/conversation-store-test.js +138 -138
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/embedding-config-test.js +32 -32
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/fake-browser-exit.js +10 -10
- package/_site/_test/fixtures/fake-browser-hang.js +10 -10
- package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
- package/_site/_test/fixtures/make-git-repos.js +92 -92
- package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/fresh-hook-reimport-test.js +53 -53
- package/_site/_test/fresh-product-data-test.js +56 -56
- package/_site/_test/full-integration-e2e-test.js +167 -167
- package/_site/_test/git-path-compatibility-test.js +80 -80
- package/_site/_test/git-validation-test.js +81 -81
- package/_site/_test/glm-ui-refresh-review-test.js +313 -404
- package/_site/_test/harness-isolation-test.js +138 -0
- package/_site/_test/helpers/cdp-browser.js +383 -0
- package/_site/_test/helpers/cdp-network-capture.js +66 -0
- package/_site/_test/helpers/find-chrome.js +34 -0
- package/_site/_test/helpers/free-port.js +8 -0
- package/_site/_test/helpers/logging-ui-fixture.js +131 -0
- package/_site/_test/helpers/module-fixture.js +282 -0
- package/_site/_test/helpers/parallel-engine-advanced-engine-child.js +45 -0
- package/_site/_test/helpers/parallel-engine-advanced-fault-writer.js +171 -0
- package/_site/_test/helpers/parallel-engine-advanced-harness.js +245 -0
- package/_site/_test/helpers/parallel-engine-advanced-standin-analyzer.js +80 -0
- package/_site/_test/helpers/parallel-engine-advanced-ui-host.js +177 -0
- package/_site/_test/helpers/parallel-engine-final-harness.js +314 -0
- package/_site/_test/helpers/parallel-engine-final-standin-analyzer.js +159 -0
- package/_site/_test/helpers/parallel-knowledge-fixture.js +255 -0
- package/_site/_test/helpers/parallel-logging-acceptance-fixture.js +111 -0
- package/_site/_test/helpers/parallel-logging-acceptance-proxy.js +106 -0
- package/_site/_test/helpers/parallel-logging-fix-r2-proxy.js +205 -0
- package/_site/_test/helpers/parallel-logging-integration-5025-env.js +432 -0
- package/_site/_test/helpers/parallel-memory-analyzer.js +81 -0
- package/_site/_test/helpers/parallel-memory-event-bridge.js +128 -0
- package/_site/_test/helpers/parallel-memory-shell.js +470 -0
- package/_site/_test/helpers/parallel-nav-isolation-env.js +389 -0
- package/_site/_test/helpers/parallel-nav-module-ui.js +186 -0
- package/_site/_test/helpers/parallel-nav-proxy.js +148 -0
- package/_site/_test/helpers/parallel-nav-shell.js +361 -0
- package/_site/_test/helpers/parallel-qa-engine-endpoint-guard.js +206 -0
- package/_site/_test/helpers/parallel-qa-engine-endpoint-isolate.js +257 -0
- package/_site/_test/helpers/parallel-retrieval-followup-corpus.js +293 -0
- package/_site/_test/helpers/parallel-retrieval-followup-next-session.js +70 -0
- package/_site/_test/helpers/parallel-retrieval-followup-r2-shell.js +737 -0
- package/_site/_test/helpers/parallel-retrieval-followup-r2-wb-host.js +82 -0
- package/_site/_test/helpers/parallel-retrieval-followup-shell.js +279 -0
- package/_site/_test/helpers/parallel-workbench-fixture.js +371 -0
- package/_site/_test/helpers/parallel-workbench-host.js +80 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-fixture.js +408 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-harness.js +593 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-host.js +70 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-launcher.js +240 -0
- package/_site/_test/helpers/parallel-workbench-integration-5025-runtime-harness.js +630 -0
- package/_site/_test/helpers/parallel-workbench-source-map.js +196 -0
- package/_site/_test/helpers/spawn-server.js +106 -0
- package/_site/_test/helpers/version-visibility-fixture.js +251 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/hook-worktree-test.js +42 -42
- package/_site/_test/import-preflight-api-test.js +234 -234
- package/_site/_test/index-builder-test.js +41 -41
- package/_site/_test/index-error-display-contract-test.js +10 -10
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +262 -262
- package/_site/_test/integration-auto-repair-test.js +113 -113
- package/_site/_test/integration-capture-setup-test.js +118 -118
- package/_site/_test/integration-intent-test.js +81 -81
- package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
- package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
- package/_site/_test/integration-runtime-binding-test.js +473 -473
- package/_site/_test/integration-status-scan-test.js +29 -29
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/kb-framework-test.js +57 -57
- package/_site/_test/kbpath-follow-test.js +63 -63
- package/_site/_test/knowledge-db-test.js +144 -144
- package/_site/_test/knowledge-engine-client-test.js +120 -109
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-maintenance-test.js +40 -40
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-scopes-test.js +37 -37
- package/_site/_test/knowledge-storage-location-test.js +28 -28
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/launcher-readiness-test.js +80 -80
- package/_site/_test/legacy-claude-session-alias-test.js +106 -106
- package/_site/_test/legacy-routes-removed-test.js +47 -47
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
- package/_site/_test/live-gateway-test.js +48 -48
- package/_site/_test/local-marketplace-test.js +16 -16
- package/_site/_test/log-redaction-test.js +58 -58
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +107 -107
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/markdown-maintenance-test.js +11 -11
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/model-context-windows-test.js +89 -89
- package/_site/_test/module-agent-start-proxy-test.js +65 -65
- package/_site/_test/module-artifact-integrity-test.js +27 -27
- package/_site/_test/module-boundary-test.js +30 -30
- package/_site/_test/module-bridge-eventbridge-test.js +202 -202
- package/_site/_test/module-dynamic-port-test.js +118 -118
- package/_site/_test/module-index-proxy-test.js +33 -33
- package/_site/_test/module-model-configuration-test.js +44 -44
- package/_site/_test/module-process-lifecycle-test.js +35 -35
- package/_site/_test/module-stream-proxy-test.js +34 -34
- package/_site/_test/module-terminal-restart-test.js +63 -63
- package/_site/_test/module-wiring-unbind-race-test.js +59 -59
- package/_site/_test/native-context-integrations-test.js +86 -86
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/orphaned-claim-recovery-test.js +80 -80
- package/_site/_test/package-boundary-test.js +43 -43
- package/_site/_test/package-startup-test.js +56 -56
- package/_site/_test/packaged-ui-smoke-test.js +88 -88
- package/_site/_test/parallel-ci-r9-posix-socket-budget-test.js +156 -0
- package/_site/_test/parallel-engine-advanced-eng005-test.js +136 -0
- package/_site/_test/parallel-engine-advanced-eng006-test.js +171 -0
- package/_site/_test/parallel-engine-advanced-eng007-reconcile-test.js +231 -0
- package/_site/_test/parallel-engine-advanced-eng009-test.js +208 -0
- package/_site/_test/parallel-engine-advanced-eng010-test.js +213 -0
- package/_site/_test/parallel-engine-advanced-eng011-ui-probe.js +459 -0
- package/_site/_test/parallel-engine-final-eng002-test.js +255 -0
- package/_site/_test/parallel-engine-final-eng007-test.js +269 -0
- package/_site/_test/parallel-engine-final-eng011-test.js +197 -0
- package/_site/_test/parallel-engine-final-eng012-test.js +354 -0
- package/_site/_test/parallel-knowledge-client-error-code-test.js +184 -0
- package/_site/_test/parallel-knowledge-engine-test.js +353 -0
- package/_site/_test/parallel-knowledge-retrieval-test.js +292 -0
- package/_site/_test/parallel-knowledge-vector-test.js +168 -0
- package/_site/_test/parallel-linux-ci-r7-log007-day-test.js +110 -0
- package/_site/_test/parallel-logging-acceptance-log003-test.js +224 -0
- package/_site/_test/parallel-logging-acceptance-log004-test.js +231 -0
- package/_site/_test/parallel-logging-acceptance-log005-test.js +208 -0
- package/_site/_test/parallel-logging-acceptance-log006-test.js +214 -0
- package/_site/_test/parallel-logging-acceptance-log007-test.js +223 -0
- package/_site/_test/parallel-logging-acceptance-log008-test.js +180 -0
- package/_site/_test/parallel-logging-fix-r2-crash-probe.js +271 -0
- package/_site/_test/parallel-logging-fix-r2-gap-test.js +294 -0
- package/_site/_test/parallel-logging-fix-r2-normalhook-test.js +330 -0
- package/_site/_test/parallel-logging-integration-5025-portability-test.js +554 -0
- package/_site/_test/parallel-memory-candidate-test.js +301 -0
- package/_site/_test/parallel-memory-manual-test.js +197 -0
- package/_site/_test/parallel-memory-scope-test.js +228 -0
- package/_site/_test/parallel-memory-source-test.js +227 -0
- package/_site/_test/parallel-memory-sync-test.js +238 -0
- package/_site/_test/parallel-memory-view-r8-reload-gate-test.js +123 -0
- package/_site/_test/parallel-nav-failure-test.js +119 -0
- package/_site/_test/parallel-nav-isolation-test.js +308 -0
- package/_site/_test/parallel-nav-isolation-vector-path-test.js +90 -0
- package/_site/_test/parallel-nav-offline-test.js +272 -0
- package/_site/_test/parallel-nav-operability-test.js +257 -0
- package/_site/_test/parallel-nav-switch-test.js +247 -0
- package/_site/_test/parallel-nav-theme-test.js +130 -0
- package/_site/_test/parallel-nav-viewport-test.js +310 -0
- package/_site/_test/parallel-qa-engine-endpoint-guard-test.js +273 -0
- package/_site/_test/parallel-retrieval-followup-citation-test.js +219 -0
- package/_site/_test/parallel-retrieval-followup-corpus-test.js +287 -0
- package/_site/_test/parallel-workbench-independent-host.js +262 -0
- package/_site/_test/parallel-workbench-integration-5025-abort-probe.js +340 -0
- package/_site/_test/parallel-workbench-integration-5025-drafts-probe.js +588 -0
- package/_site/_test/parallel-workbench-integration-5025-env-check.js +317 -0
- package/_site/_test/parallel-workbench-integration-5025-model-probe.js +342 -0
- package/_site/_test/parallel-workbench-integration-5025-ordering-probe.js +326 -0
- package/_site/_test/parallel-workbench-model-probe.js +336 -0
- package/_site/_test/parallel-workbench-offline-test.js +169 -0
- package/_site/_test/parallel-workbench-provenance-trace.js +134 -0
- package/_site/_test/parallel-workbench-source-match.js +206 -0
- package/_site/_test/parallel-workbench-standalone-turn.js +349 -0
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/pr-consumer-contract-test.js +55 -55
- package/_site/_test/product-brand-contract-test.js +25 -25
- package/_site/_test/product-diagnostics-ui-test.js +93 -93
- package/_site/_test/product-empty-state-test.js +148 -0
- package/_site/_test/product-import-ui-test.js +219 -104
- package/_site/_test/product-navigation-test.js +152 -0
- package/_site/_test/product-progress-status-test.js +84 -84
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-domain-create-test.js +91 -0
- package/_site/_test/project-domain-scope-retrieval-test.js +214 -0
- package/_site/_test/project-git-init-confirm-test.js +115 -0
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-import-duplicate-alias-test.js +134 -0
- package/_site/_test/project-import-unicode-restart-test.js +140 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/project-module-partial-recovery-test.js +222 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -79
- package/_site/_test/project-store-test.js +90 -90
- package/_site/_test/project-unbind-late-read-ui-test.js +250 -0
- package/_site/_test/project-unbind-writer-guard-test.js +231 -0
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/registry-events-test.js +38 -38
- package/_site/_test/release-version-sync-test.js +115 -115
- package/_site/_test/requirement-binding-test.js +113 -113
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +173 -161
- package/_site/_test/runtime-endpoint-test.js +64 -64
- package/_site/_test/scanner-test.js +83 -83
- package/_site/_test/server-security-test.js +96 -96
- package/_site/_test/sessions-stream-test.js +93 -93
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/shared-scope-root-binding-test.js +74 -67
- package/_site/_test/simple-import-test.js +98 -98
- package/_site/_test/startup-analysis-disabled-test.js +16 -16
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/t07b-cln-005-coverage-test.js +212 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
- package/_site/_test/team-knowledge-store-test.js +132 -132
- package/_site/_test/test-server-cleanup-test.js +35 -35
- package/_site/_test/test-server-module-isolation-test.js +37 -37
- package/_site/_test/tracking-start-test.js +144 -144
- package/_site/_test/vector-storage-root-test.js +75 -75
- package/_site/_test/version-visibility-browser-probe.js +249 -0
- package/_site/_test/version-visibility-test.js +92 -0
- package/_site/_test/workbench-context-receipts-test.js +22 -22
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/zcode-capture-integration-test.js +112 -112
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-vendor-presets.js +83 -83
- package/_site/lib/ai-workspace.js +104 -104
- package/_site/lib/atomic-file.js +204 -204
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +260 -260
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/capture-hook-config.js +108 -108
- package/_site/lib/claude-cli-runner.js +1599 -1599
- package/_site/lib/cli-instances.js +43 -43
- package/_site/lib/codex-context-status.js +17 -17
- package/_site/lib/codex-notify-config.js +193 -193
- package/_site/lib/commit-conversation-binder.js +247 -247
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +444 -444
- package/_site/lib/context-pack-builder.js +299 -299
- package/_site/lib/contracts.js +291 -291
- package/_site/lib/conversation-exclusions.js +204 -204
- package/_site/lib/conversation-query-service.js +161 -161
- package/_site/lib/conversation-store.js +387 -387
- package/_site/lib/data-dir.js +13 -13
- package/_site/lib/embedding-service.js +85 -85
- package/_site/lib/engine-connection.js +93 -93
- package/_site/lib/engine-knowledge-client.js +76 -72
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +24 -24
- package/_site/lib/git-path.js +14 -14
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1175 -1175
- package/_site/lib/hook-manager.js +203 -203
- package/_site/lib/index-builder.js +157 -157
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-auto-repair.js +77 -77
- package/_site/lib/integration-installer.js +1240 -1240
- package/_site/lib/integration-intent.js +88 -88
- package/_site/lib/integration-status-scan.js +99 -99
- package/_site/lib/integration-status-worker.js +14 -14
- package/_site/lib/kb-framework.js +191 -191
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-db.js +363 -363
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-schema.js +121 -121
- package/_site/lib/knowledge-tool-runtime.js +366 -366
- package/_site/lib/live-gateway.js +78 -78
- package/_site/lib/llm-client.js +173 -173
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/model-context-windows.js +82 -82
- package/_site/lib/module-bridge.js +874 -874
- package/_site/lib/module-port-resolver.js +57 -57
- package/_site/lib/post-commit-automation.js +141 -141
- package/_site/lib/product-data.js +35 -35
- package/_site/lib/project-lifecycle-service.js +554 -554
- package/_site/lib/project-registry-store.js +129 -129
- package/_site/lib/project-store.js +268 -268
- package/_site/lib/prompt-registry.js +148 -148
- package/_site/lib/public-version-info.js +37 -0
- package/_site/lib/registry-events.js +16 -16
- package/_site/lib/requirement-adapters.js +10 -10
- package/_site/lib/requirement-binder.js +112 -112
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-control.js +89 -89
- package/_site/lib/runtime-endpoint.js +160 -160
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1672 -1664
- package/_site/lib/settings-store.js +153 -153
- package/_site/lib/storage-layout.js +166 -166
- package/_site/lib/structured-logger.js +574 -574
- package/_site/lib/zcode-context-integration.js +60 -60
- package/_site/scripts/codex-notify-fanout.cjs +95 -95
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +158 -158
- package/_site/scripts/install-module-candidates.js +69 -69
- package/_site/scripts/pack-module-candidates.js +89 -89
- package/_site/scripts/sync-product-content.js +28 -28
- package/_site/scripts/sync-release-version.js +164 -164
- package/_site/scripts/vendor-modules.js +49 -49
- package/_site/scripts/verify-module-runtime.js +50 -50
- package/_site/scripts/verify-product-runtime.js +36 -36
- package/_site/scripts/verify-retrieval-gold.js +83 -83
- package/_site/server.js +15 -15
- package/_site/start.bat +26 -26
- package/_site/stop.bat +11 -11
- package/bin/project-knowledge-integrations.js +254 -254
- package/bin/project-knowledge-kb.js +67 -67
- package/bin/project-knowledge-mcp.js +201 -201
- package/bin/project-knowledge.js +269 -170
- package/claude-prompts.json +21 -21
- package/docs/README.zh-CN.md +207 -205
- package/docs/assets/architecture.svg +181 -181
- package/docs/assets/logo.svg +49 -49
- package/docs/fresh-v5-start.md +44 -44
- package/docs/leader-glm-workbench-playbook.md +122 -122
- package/docs/project-registry-schema.md +33 -33
- package/docs/release-5.0.16-acceptance.md +176 -176
- package/docs/release-5.0.17-acceptance.md +117 -117
- package/docs/release-5.0.19-acceptance.md +39 -39
- package/docs/team-knowledge-mode-a-plan.md +73 -73
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +68 -65
- package/package.json +113 -110
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/templates/change.md +30 -30
- package/templates/project-readme.md +52 -52
- package/ui/index.html +3 -2
- package/ui/version-info.css +44 -0
- package/ui/version-info.js +24 -0
- 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; });
|