@sanqianx/project-knowledge 5.0.23 → 5.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -20
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +1361 -1346
- 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 +70 -67
- 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,231 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// P-ENGINE-REVIEW-R2 ENG-007 boundary re-check: what exactly does 启动对账 cover?
|
|
4
|
+
//
|
|
5
|
+
// Contract verified against the current docs and the real implementation
|
|
6
|
+
// BEFORE this driver was written:
|
|
7
|
+
// * README.md (触发面): "消费 event-bridge journal 的 commit boundary 与关联
|
|
8
|
+
// 会话事件 -> CommitReconciler 对账/认领" - the reconciliation source is the
|
|
9
|
+
// JOURNAL, not Git history.
|
|
10
|
+
// * README.md:119: "未启动服务时,Hook 已捕获的事件保留于 journal,服务消费后
|
|
11
|
+
// 才生成分析任务" - retention/compensation scope is events the hook captured.
|
|
12
|
+
// * src/coordinator.js consumeJournal(): the only compensation path reads
|
|
13
|
+
// journal events from cursor+1; there is no git-log scan anywhere.
|
|
14
|
+
// * src/runtime/commit-reconciler.js:173: "failed SHA is never retried,
|
|
15
|
+
// scanned, or inferred from history."
|
|
16
|
+
// => The startup reconciliation contract is JOURNAL RECOVERY. A commit whose
|
|
17
|
+
// hook never wrote a boundary event is outside that contract (uncovered
|
|
18
|
+
// boundary, honestly stated - not papered over as "compensated").
|
|
19
|
+
//
|
|
20
|
+
// This driver pins BOTH SHAs of the offline window and answers the Leader's
|
|
21
|
+
// question directly:
|
|
22
|
+
// commit A - broken hook (no boundary event) -> NOT found back by any Git
|
|
23
|
+
// reconcile (no such mechanism exists).
|
|
24
|
+
// commit B - working hook (boundary event) -> compensated by the startup
|
|
25
|
+
// journal drain (the ONLY recovery path).
|
|
26
|
+
// Then, decisively: a REAL boundary event for A is delivered after the fact
|
|
27
|
+
// with the real bridge CLI. The event is consumed, but B's success moved
|
|
28
|
+
// lastAnalyzedCommit to shaB, so the already-analysed-ancestor guard skips A
|
|
29
|
+
// (A is B's parent in real Git history): A is skipped over by B even when
|
|
30
|
+
// its evidence is handed to the engine explicitly. A is not analysed, no run
|
|
31
|
+
// is created - the guarantee "已完成SHA不重跑" extends to ancestors of the
|
|
32
|
+
// analysed head.
|
|
33
|
+
//
|
|
34
|
+
// Not repeated here (already green in engine-final/eng007, untouched): the
|
|
35
|
+
// duplicate-B replay and the restart-no-rerun saga.
|
|
36
|
+
|
|
37
|
+
const assert = require('node:assert/strict');
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
const { spawnSync } = require('node:child_process');
|
|
41
|
+
const {
|
|
42
|
+
makeLaneDir, buildFixtureRepo, installPostCommitHook, startBridge, startVector,
|
|
43
|
+
startEngine, waitUntil, git, gitRun, readJsonl, saveEvidence, isolatedEnv, BRIDGE_CLI,
|
|
44
|
+
} = require('./helpers/parallel-engine-final-harness.js');
|
|
45
|
+
const { createStandinAnalyzer } = require('./helpers/parallel-engine-final-standin-analyzer.js');
|
|
46
|
+
|
|
47
|
+
const ARTIFACTS = process.env.PK_TEST_EVIDENCE_ROOT
|
|
48
|
+
? path.join(process.env.PK_TEST_EVIDENCE_ROOT, 'eng007reconcile')
|
|
49
|
+
: path.resolve(__dirname, '../../qa-autopilot/artifacts/parallel-20261023/engine-review-r3/reruns/eng007reconcile');
|
|
50
|
+
const PROJECT_ID = 'project-eng007r2';
|
|
51
|
+
|
|
52
|
+
function findJournalFiles(root) {
|
|
53
|
+
const out = [];
|
|
54
|
+
const walk = dir => {
|
|
55
|
+
if (!fs.existsSync(dir)) return;
|
|
56
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
57
|
+
const absolute = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) walk(absolute);
|
|
59
|
+
else if (entry.name === 'events.jsonl') out.push(absolute);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
walk(root);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function boundaryEvents(...roots) {
|
|
66
|
+
const events = [];
|
|
67
|
+
for (const file of roots.flatMap(root => findJournalFiles(root))) {
|
|
68
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
69
|
+
const event = JSON.parse(line);
|
|
70
|
+
if (event.schema === 'git-commit-boundary/v1') events.push(event);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return events;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
const lane = makeLaneDir('eng007r2');
|
|
78
|
+
const commands = [];
|
|
79
|
+
const evidence = {
|
|
80
|
+
caseId: 'ENG-007 (R2 boundary re-check: startup reconciliation contract scope, both SHAs)',
|
|
81
|
+
laneDir: lane,
|
|
82
|
+
contract: {
|
|
83
|
+
source: 'README.md 触发面 + README.md:119 + src/coordinator.js consumeJournal (journal events are the only compensation source; no git-log scan) + src/runtime/commit-reconciler.js:173 (failed SHA never retried/scanned/inferred from history)',
|
|
84
|
+
scope: 'startup reconciliation = JOURNAL RECOVERY only; commits whose hook never wrote a boundary event are NOT covered (uncovered boundary, by design and by docs)',
|
|
85
|
+
},
|
|
86
|
+
phases: {},
|
|
87
|
+
commands,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const fixture = buildFixtureRepo(lane, 'eng007r2', []);
|
|
91
|
+
const knowledgePath = path.join(lane, 'knowledge-eng007r2');
|
|
92
|
+
const storePath = path.join(knowledgePath, 'dev-conversations');
|
|
93
|
+
fs.mkdirSync(storePath, { recursive: true });
|
|
94
|
+
const home = path.join(lane, 'bridge-home');
|
|
95
|
+
const dataDir = path.join(lane, 'engine-data');
|
|
96
|
+
const vectorRoot = path.join(lane, 'vector-root');
|
|
97
|
+
const eventsFile = path.join(lane, 'analyzer-events.jsonl');
|
|
98
|
+
|
|
99
|
+
const bridge = startBridge(home);
|
|
100
|
+
let engine = null;
|
|
101
|
+
let vector = null;
|
|
102
|
+
const analyzer = createStandinAnalyzer({ eventsFile });
|
|
103
|
+
const invocationsFor = sha => readJsonl(eventsFile).filter(entry => entry.kind === 'start' && entry.commitSha === sha).length;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const bridgeUrl = await bridge.listen();
|
|
107
|
+
vector = await startVector(vectorRoot);
|
|
108
|
+
const registered = await bridge.projectRegistry.addProject({ home, path: fixture.repoPath, store: storePath });
|
|
109
|
+
const { repoIdentity } = await bridge.resolveRepoContext(fixture.repoPath);
|
|
110
|
+
|
|
111
|
+
// Register the project (consumer off), then stop the engine - the offline window begins.
|
|
112
|
+
engine = await startEngine({ dataDir, bridgeUrl, vectorUrl: vector.url, analyzer, consume: false });
|
|
113
|
+
await engine.listen();
|
|
114
|
+
engine.runtime.store.addProject({
|
|
115
|
+
projectId: PROJECT_ID, repoPath: fixture.repoPath, knowledgePath,
|
|
116
|
+
trackingStartCommit: fixture.baseline, repoIdentity, journalId: registered.id, conversationBaselineCursor: 0,
|
|
117
|
+
});
|
|
118
|
+
await engine.close();
|
|
119
|
+
engine = null;
|
|
120
|
+
commands.push('engine lifecycle 0: project registered (consumer off), engine stopped - offline window begins');
|
|
121
|
+
|
|
122
|
+
// ----- commit A: BROKEN hook (engine down, no boundary event) -----
|
|
123
|
+
installPostCommitHook(fixture.repoPath, { home, mode: 'broken' });
|
|
124
|
+
fs.writeFileSync(path.join(fixture.repoPath, 'offline-a.txt'), `offline feature A\n${'a'.repeat(64)}\n`);
|
|
125
|
+
git(fixture.repoPath, ['add', '.']);
|
|
126
|
+
const commitA = gitRun(fixture.repoPath, ['commit', '-qm', 'offline feature A']);
|
|
127
|
+
assert.equal(commitA.code, 0, `Git must not be blocked by the failing hook (exit ${commitA.code})`);
|
|
128
|
+
const shaA = git(fixture.repoPath, ['rev-parse', 'HEAD']);
|
|
129
|
+
assert.match(commitA.stderr, /commit-boundary\.missing\.js/, 'the hook really failed');
|
|
130
|
+
|
|
131
|
+
// ----- commit B: WORKING hook (engine down, boundary event on disk) -----
|
|
132
|
+
installPostCommitHook(fixture.repoPath, { home, mode: 'ok' });
|
|
133
|
+
fs.writeFileSync(path.join(fixture.repoPath, 'offline-b.txt'), `offline feature B\n${'b'.repeat(64)}\n`);
|
|
134
|
+
git(fixture.repoPath, ['add', '.']);
|
|
135
|
+
const commitB = gitRun(fixture.repoPath, ['commit', '-qm', 'offline feature B']);
|
|
136
|
+
assert.equal(commitB.code, 0);
|
|
137
|
+
const shaB = git(fixture.repoPath, ['rev-parse', 'HEAD']);
|
|
138
|
+
const boundariesOffline = boundaryEvents(home, storePath);
|
|
139
|
+
assert.equal(boundariesOffline.filter(e => e.commitSha === shaB).length, 1, 'B wrote exactly one boundary event while the engine was down');
|
|
140
|
+
assert.equal(boundariesOffline.filter(e => e.commitSha === shaA).length, 0, 'A wrote no boundary event');
|
|
141
|
+
|
|
142
|
+
// A really is B's parent in real Git history (the "skipped over by B" precondition).
|
|
143
|
+
const aIsAncestorOfB = gitRun(fixture.repoPath, ['merge-base', '--is-ancestor', shaA, shaB]);
|
|
144
|
+
assert.equal(aIsAncestorOfB.code, 0, 'A must be an ancestor of B in the fixture history');
|
|
145
|
+
|
|
146
|
+
// ----- restart: startup drain compensates B only -----
|
|
147
|
+
engine = await startEngine({ dataDir, bridgeUrl, vectorUrl: vector.url, analyzer, consume: true });
|
|
148
|
+
await engine.listen();
|
|
149
|
+
const runB = await waitUntil(async () => {
|
|
150
|
+
const run = engine.runtime.store.listRuns(PROJECT_ID).find(r => r.commitSha === shaB);
|
|
151
|
+
return run && run.state === 'succeeded' ? run : null;
|
|
152
|
+
}, 60000, 'startup drain compensates B');
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // let the drain settle fully before judging A
|
|
154
|
+
const runsAfterStart = engine.runtime.store.listRuns(PROJECT_ID);
|
|
155
|
+
const ledgerA = path.join(dataDir, 'runtime', 'processed-commits', PROJECT_ID, `${shaA}.json`);
|
|
156
|
+
const ledgerB = path.join(dataDir, 'runtime', 'processed-commits', PROJECT_ID, `${shaB}.json`);
|
|
157
|
+
const lastAnalyzed = engine.runtime.store.getProject(PROJECT_ID)?.runtime?.lastAnalyzedCommit || null;
|
|
158
|
+
evidence.phases.offlineWindow = {
|
|
159
|
+
commitA: { sha: shaA, subject: 'offline feature A', hook: 'broken (missing CLI)', gitExit: commitA.code, boundaryEvents: 0 },
|
|
160
|
+
commitB: { sha: shaB, subject: 'offline feature B', hook: 'working (real bridge CLI)', gitExit: commitB.code, boundaryEvents: 1 },
|
|
161
|
+
aIsAncestorOfB: aIsAncestorOfB.code === 0,
|
|
162
|
+
startupCompensation: {
|
|
163
|
+
runForB: { runId: runB.runId, trigger: runB.trigger, state: runB.state, indexState: runB.indexState },
|
|
164
|
+
runsForA: runsAfterStart.filter(r => r.commitSha === shaA).length,
|
|
165
|
+
totalRuns: runsAfterStart.length,
|
|
166
|
+
analyzerInvocations: { forA: invocationsFor(shaA), forB: invocationsFor(shaB) },
|
|
167
|
+
ledgerAExists: fs.existsSync(ledgerA),
|
|
168
|
+
ledgerBStatus: fs.existsSync(ledgerB) ? JSON.parse(fs.readFileSync(ledgerB, 'utf8')).status : null,
|
|
169
|
+
lastAnalyzedCommit: lastAnalyzed,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
assert.equal(runB.trigger, 'git-hook');
|
|
173
|
+
assert.equal(invocationsFor(shaB), 1, 'B compensated exactly once');
|
|
174
|
+
assert.equal(runsAfterStart.filter(r => r.commitSha === shaA).length, 0, 'A has no run: the startup drain found nothing for A (no boundary event, no Git-history scan)');
|
|
175
|
+
assert.equal(invocationsFor(shaA), 0, 'A was never analysed');
|
|
176
|
+
assert.equal(fs.existsSync(ledgerA), false, 'A is not in the processing ledger');
|
|
177
|
+
assert.equal(lastAnalyzed, shaB, 'B\'s success moved lastAnalyzedCommit to shaB');
|
|
178
|
+
commands.push(`restart -> drain compensated B (${shaB.slice(0, 8)}) only; A (${shaA.slice(0, 8)}) has no run/ledger/analysis`);
|
|
179
|
+
|
|
180
|
+
// ----- decisive check: hand A's boundary event to the engine AFTER the fact -----
|
|
181
|
+
const cursorBefore = engine.runtime.store.state.journalCursors[registered.id] || 0;
|
|
182
|
+
const replayA = spawnSync(process.execPath, [BRIDGE_CLI, '--home', home, '--cwd', fixture.repoPath, '--sha', shaA], {
|
|
183
|
+
cwd: fixture.repoPath, windowsHide: true, encoding: 'utf8', env: isolatedEnv(),
|
|
184
|
+
});
|
|
185
|
+
assert.equal(replayA.status, 0, `replaying A's boundary event failed: ${replayA.stderr}`);
|
|
186
|
+
await waitUntil(async () => (engine.runtime.store.state.journalCursors[registered.id] || 0) > cursorBefore, 20000, 'A\'s after-the-fact boundary event is consumed');
|
|
187
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
188
|
+
const runsAfterReplayA = engine.runtime.store.listRuns(PROJECT_ID);
|
|
189
|
+
evidence.phases.afterTheFactBoundaryForA = {
|
|
190
|
+
replay: { ok: JSON.parse(replayA.stdout).ok, commitSha: shaA },
|
|
191
|
+
journalCursorBefore: cursorBefore,
|
|
192
|
+
journalCursorAfter: engine.runtime.store.state.journalCursors[registered.id] || 0,
|
|
193
|
+
runsForA: runsAfterReplayA.filter(r => r.commitSha === shaA).length,
|
|
194
|
+
totalRuns: runsAfterReplayA.length,
|
|
195
|
+
analyzerInvocationsForA: invocationsFor(shaA),
|
|
196
|
+
ledgerAExists: fs.existsSync(ledgerA),
|
|
197
|
+
verdict: 'A\'s evidence IS consumed (cursor advanced, boundary projection written) but the already-analysed-ancestor guard (baseline=lastAnalyzedCommit=shaB) skips A: no run, no analysis. A is skipped over by B; it is not "found back" by any Git reconcile - and none exists in the engine.',
|
|
198
|
+
};
|
|
199
|
+
assert.equal(runsAfterReplayA.filter(r => r.commitSha === shaA).length, 0, 'even with its boundary event delivered after the fact, A must not be re-analysed (ancestor guard)');
|
|
200
|
+
assert.equal(invocationsFor(shaA), 0, 'the analyzer was never invoked for A');
|
|
201
|
+
assert.equal(runsAfterReplayA.length, 1, 'the run set stays at exactly B\'s run');
|
|
202
|
+
commands.push(`A's boundary event delivered after the fact -> consumed (cursor ${cursorBefore}->${evidence.phases.afterTheFactBoundaryForA.journalCursorAfter}), ancestor guard skipped A (no run, no analysis)`);
|
|
203
|
+
|
|
204
|
+
evidence.verdict = {
|
|
205
|
+
'question A是否被Git reconcile找回': 'NO - the engine has no Git-history reconciliation at all; the only compensation source is the bridge journal (README 触发面 + README:119, src/coordinator.js consumeJournal, src/runtime/commit-reconciler.js:173)',
|
|
206
|
+
'question A是否被B越过': 'YES - B\'s promotion moved lastAnalyzedCommit to shaB; even when A\'s boundary event was delivered after the fact with the real CLI, the already-analysed-ancestor guard skipped A (consumed, projected, never enqueued)',
|
|
207
|
+
'contract scope (honest)': '启动对账 = journal 恢复:只有成功写入 boundary 事件的提交会被补回。无 boundary 事件的提交(hook 故障)不在契约内,本 driver 将其作为未覆盖边界显式记录,不计入"已补偿"',
|
|
208
|
+
'expected1 Git不被Hook故障阻塞': 'PASS (commit A exit 0 with a failing hook; commit exists)',
|
|
209
|
+
'expected2 启动对账补漏': 'PASS within the journal-recovery contract (B compensated via startup drain; A uncovered by design - stated, not claimed)',
|
|
210
|
+
'expected3 已完成SHA不重跑': 'PASS (B analysed once; ledger + ancestor guards held; A skipped as analysed-ancestor)',
|
|
211
|
+
boundary: 'unchanged paths (duplicate-B replay, restart-no-rerun) stay covered by engine-final/eng007 first-round evidence; they were not re-run here',
|
|
212
|
+
analyzerStandin: 'deterministic stand-in - NO real model output covered; vector-hub real with deterministic test embeddings - paid embedding API NOT covered',
|
|
213
|
+
};
|
|
214
|
+
await engine.close();
|
|
215
|
+
await vector.close();
|
|
216
|
+
await bridge.close();
|
|
217
|
+
saveEvidence(ARTIFACTS, 'eng007-reconcile-evidence.json', evidence);
|
|
218
|
+
console.log('ENG-007 R2 reconcile re-check PASS');
|
|
219
|
+
process.exit(0);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
evidence.verdict = { failed: String((error && error.stack) || error) };
|
|
222
|
+
try { await engine?.close(); } catch {}
|
|
223
|
+
try { await vector?.close(); } catch {}
|
|
224
|
+
try { await bridge.close(); } catch {}
|
|
225
|
+
saveEvidence(ARTIFACTS, 'eng007-reconcile-evidence.json', evidence);
|
|
226
|
+
console.error(`ENG-007 R2 reconcile re-check FAIL: ${error.message}`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
main();
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// P-ENG-ADVANCED ENG-009 晋升中断恢复 (promotion interruption recovery) acceptance driver.
|
|
4
|
+
//
|
|
5
|
+
// Case (qa-autopilot/config/test-cases.json ENG-009, P0):
|
|
6
|
+
// steps: 在许可的故障注入点中断测试writer,再重启。
|
|
7
|
+
// expected: 1) journal恢复一致。 2) 不留下半晋升的假成功。 3) pointer顺序正确。
|
|
8
|
+
//
|
|
9
|
+
// Layer A (server-level real crash): the vendored engine child is HARD-KILLED
|
|
10
|
+
// (uncatchable SIGKILL on POSIX, TerminateProcess on Windows - no graceful
|
|
11
|
+
// cleanup) while a real run is mid-analysis, then a second engine child starts
|
|
12
|
+
// on the same data directory - exercising the real single-writer stale-lock
|
|
13
|
+
// break, initialize() recoverAll(), run recovery (running -> queued/recovering),
|
|
14
|
+
// and claim retry semantics.
|
|
15
|
+
//
|
|
16
|
+
// Layer B (fault-point crashes): the vendored KnowledgePromotionService's own
|
|
17
|
+
// production `fault` constructor hook hard-exits a child process at three
|
|
18
|
+
// permitted phases (journal-prepared / operation-applied:0 / promotion-verified,
|
|
19
|
+
// journal already persisted as awaiting-state-advance), then a fresh process
|
|
20
|
+
// runs the same recoverAll() the coordinator runs at every startup.
|
|
21
|
+
//
|
|
22
|
+
// Analyzer = declarative stand-in (NO real model output). Vector endpoint =
|
|
23
|
+
// loopback double.
|
|
24
|
+
|
|
25
|
+
const assert = require('node:assert/strict');
|
|
26
|
+
const { spawn } = require('node:child_process');
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const crypto = require('node:crypto');
|
|
30
|
+
const {
|
|
31
|
+
makeLaneDir, buildFixtureRepo, waitUntil, callJson, startVectorDouble,
|
|
32
|
+
startEngineChild, readEventsFile, writePlan, saveEvidence, isolatedEnv,
|
|
33
|
+
} = require('./helpers/parallel-engine-advanced-harness.js');
|
|
34
|
+
|
|
35
|
+
const ARTIFACTS = process.env.PK_TEST_EVIDENCE_ROOT
|
|
36
|
+
? path.join(process.env.PK_TEST_EVIDENCE_ROOT, 'eng009')
|
|
37
|
+
: path.resolve(__dirname, '../../qa-autopilot/artifacts/parallel-20261023/engine-review-r3/reruns/eng009');
|
|
38
|
+
const FAULT_WRITER = path.join(__dirname, 'helpers', 'parallel-engine-advanced-fault-writer.js');
|
|
39
|
+
const PROJECT_ID = 'project-eng009';
|
|
40
|
+
|
|
41
|
+
function runFaultWriter(mode, workDir) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const proc = spawn(process.execPath, [FAULT_WRITER, mode, workDir], { windowsHide: true, env: isolatedEnv(), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
44
|
+
let stderr = '';
|
|
45
|
+
proc.stderr.on('data', chunk => { stderr += chunk; });
|
|
46
|
+
proc.on('error', reject);
|
|
47
|
+
proc.on('close', code => resolve({ mode, code, stderr: stderr.slice(0, 500) }));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hashFile(file) {
|
|
52
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const lane = makeLaneDir('eng009');
|
|
57
|
+
const commands = [];
|
|
58
|
+
const evidence = { caseId: 'ENG-009', laneDir: lane, phases: {}, commands };
|
|
59
|
+
|
|
60
|
+
// ---------- Layer A: hard crash of a live engine child, then restart ----------
|
|
61
|
+
const fixture = buildFixtureRepo(lane, 'eng009', ['X']);
|
|
62
|
+
const knowledgePath = path.join(lane, 'knowledge-eng009');
|
|
63
|
+
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
64
|
+
const planFile = path.join(lane, 'analyzer-plan.json');
|
|
65
|
+
const eventsFile = path.join(lane, 'analyzer-events.jsonl');
|
|
66
|
+
writePlan(planFile, { default: {}, rules: [{ projectId: PROJECT_ID, commitSha: fixture.shas.X, delayMs: 5000 }] });
|
|
67
|
+
|
|
68
|
+
const vector = await startVectorDouble();
|
|
69
|
+
const dataDir = path.join(lane, 'engine-data');
|
|
70
|
+
const engine1 = await startEngineChild({ label: 'eng009', dataDir, vectorUrl: vector.url, planFile, eventsFile });
|
|
71
|
+
commands.push('LayerA: POST /api/runs X (analyzer stall 5s)', 'LayerA: hard-kill engine child mid-analysis', 'LayerA: restart engine child on same dataDir');
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
assert.equal((await callJson(engine1.base, '/api/projects', {
|
|
75
|
+
projectId: PROJECT_ID, name: 'eng009', repoPath: fixture.repoPath, knowledgePath, trackingStartCommit: fixture.baseline,
|
|
76
|
+
})).status, 200);
|
|
77
|
+
const runX = (await callJson(engine1.base, '/api/runs', { projectId: PROJECT_ID, commitSha: fixture.shas.X })).body.run;
|
|
78
|
+
await waitUntil(() => readEventsFile(eventsFile).some(e => e.kind === 'start' && e.commitSha === fixture.shas.X), 20000, 'analyzer started X');
|
|
79
|
+
|
|
80
|
+
const killCode = await engine1.killHard();
|
|
81
|
+
const stateRaw = JSON.parse(fs.readFileSync(path.join(dataDir, 'engine-state.json'), 'utf8'));
|
|
82
|
+
const crashedRun = stateRaw.runs.find(r => r.runId === runX.runId);
|
|
83
|
+
const lockFile = path.join(dataDir, 'engine-runtime.lock');
|
|
84
|
+
const lockOwner = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
85
|
+
const promotionsDir = path.join(dataDir, 'runtime', 'promotions', PROJECT_ID);
|
|
86
|
+
evidence.phases.layerACrash = {
|
|
87
|
+
killExitCode: killCode,
|
|
88
|
+
killSignal: engine1.proc.signalCode,
|
|
89
|
+
killWasUncatchable: engine1.proc.signalCode === 'SIGKILL' || process.platform === 'win32',
|
|
90
|
+
runStateOnDiskAfterKill: { state: crashedRun.state, phase: crashedRun.phase },
|
|
91
|
+
lockOwnerPid: lockOwner.pid,
|
|
92
|
+
lockOwnerPidWasEngineChild: lockOwner.pid === engine1.proc.pid,
|
|
93
|
+
promotionJournalsOnDisk: fs.existsSync(promotionsDir) ? fs.readdirSync(promotionsDir) : [],
|
|
94
|
+
pointerAtCrash: stateRaw.projects.find(p => p.projectId === PROJECT_ID).runtime.lastAnalyzedCommit,
|
|
95
|
+
};
|
|
96
|
+
assert.equal(crashedRun.state, 'running', 'run was live when the writer was interrupted');
|
|
97
|
+
assert.equal(lockOwner.pid, engine1.proc.pid, 'single-writer lock belonged to the killed engine');
|
|
98
|
+
assert.equal(evidence.phases.layerACrash.pointerAtCrash, fixture.baseline, 'pointer untouched before durable promotion');
|
|
99
|
+
|
|
100
|
+
// Restart on the same dataDir: stale lock must break via dead owner pid.
|
|
101
|
+
const engine2 = await startEngineChild({ label: 'eng009-restart', dataDir, vectorUrl: vector.url, planFile, eventsFile });
|
|
102
|
+
try {
|
|
103
|
+
const recovered = await waitUntil(async () => {
|
|
104
|
+
const runs = (await callJson(engine2.base, `/api/runs?projectId=${PROJECT_ID}`)).body.runs;
|
|
105
|
+
return runs.length === 1 && runs[0].state === 'succeeded' ? runs[0] : null;
|
|
106
|
+
}, 45000, 'restarted engine completes the interrupted run');
|
|
107
|
+
const events = readEventsFile(eventsFile);
|
|
108
|
+
const starts = events.filter(e => e.kind === 'start' && e.commitSha === fixture.shas.X);
|
|
109
|
+
const runtime = (await callJson(engine2.base, `/api/snapshot?projectId=${PROJECT_ID}`)).body.projects.find(p => p.projectId === PROJECT_ID).runtime;
|
|
110
|
+
const knowledgeFiles = fs.readdirSync(path.join(knowledgePath, 'changes'));
|
|
111
|
+
const journalFiles = fs.readdirSync(promotionsDir);
|
|
112
|
+
const journal = JSON.parse(fs.readFileSync(path.join(promotionsDir, journalFiles[0]), 'utf8'));
|
|
113
|
+
evidence.phases.layerARecovery = {
|
|
114
|
+
runId: recovered.runId,
|
|
115
|
+
sameRunIdAsBeforeCrash: recovered.runId === runX.runId,
|
|
116
|
+
state: recovered.state,
|
|
117
|
+
promotedAt: recovered.promotedAt,
|
|
118
|
+
analyzerStartsForX: starts.length,
|
|
119
|
+
attemptEvidence: 'crash before durable state advance -> engine re-runs analysis (attempt 2) rather than faking success',
|
|
120
|
+
lastAnalyzedCommit: runtime.lastAnalyzedCommit,
|
|
121
|
+
knowledgeChangesFiles: knowledgeFiles,
|
|
122
|
+
journalPhase: journal.phase,
|
|
123
|
+
journalOperationsApplied: journal.operations.map(op => ({ path: op.path, applied: op.applied })),
|
|
124
|
+
lockFileRecreatedByRestart: JSON.parse(fs.readFileSync(lockFile, 'utf8')).pid === engine2.proc.pid,
|
|
125
|
+
};
|
|
126
|
+
assert.equal(recovered.runId, runX.runId, 'no duplicate run created by recovery');
|
|
127
|
+
assert.equal(starts.length, 2, 'analyzer ran once pre-crash and once after restart (no fake success)');
|
|
128
|
+
assert.equal(runtime.lastAnalyzedCommit, fixture.shas.X, 'pointer advanced exactly to X after recovery');
|
|
129
|
+
assert.deepEqual(knowledgeFiles, [`${fixture.shas.X.slice(0, 12)}.md`], 'exactly one promoted knowledge file');
|
|
130
|
+
assert.equal(journal.phase, 'completed', 'promotion journal reaches terminal completed phase');
|
|
131
|
+
assert.ok(journal.operations.every(op => op.applied), 'all journal operations applied');
|
|
132
|
+
await engine2.stop();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
try { await engine2.stop(); } catch {}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- Layer B: fault-point crashes through the promotion service's own hook ----------
|
|
139
|
+
const scenarios = [
|
|
140
|
+
{ mode: 'crash-prepared', expect: 'rolled-back' },
|
|
141
|
+
{ mode: 'crash-mid-apply', expect: 'rolled-back' },
|
|
142
|
+
{ mode: 'crash-after-apply', expect: 'completed' },
|
|
143
|
+
];
|
|
144
|
+
evidence.phases.layerB = {};
|
|
145
|
+
for (const scenario of scenarios) {
|
|
146
|
+
const workDir = path.join(lane, `fault-${scenario.mode}`);
|
|
147
|
+
const crash = await runFaultWriter(scenario.mode, workDir);
|
|
148
|
+
commands.push(`LayerB ${scenario.mode}: fault-writer exit ${crash.code}`);
|
|
149
|
+
assert.equal(crash.code, 137, `${scenario.mode} child hard-exited at the fault point (got ${crash.code}, stderr: ${crash.stderr})`);
|
|
150
|
+
const recover = await runFaultWriter('recover', workDir);
|
|
151
|
+
assert.equal(recover.code, 0, `${scenario.mode} recovery process succeeded (${recover.stderr})`);
|
|
152
|
+
const result = JSON.parse(fs.readFileSync(path.join(workDir, 'fault-result.json'), 'utf8'));
|
|
153
|
+
const readme = fs.readFileSync(path.join(workDir, 'knowledge', 'README.md'), 'utf8');
|
|
154
|
+
const changesDir = path.join(workDir, 'knowledge', 'changes');
|
|
155
|
+
const recoverAgain = await runFaultWriter('recover', workDir);
|
|
156
|
+
const againResult = JSON.parse(fs.readFileSync(path.join(workDir, 'fault-result.json'), 'utf8'));
|
|
157
|
+
evidence.phases.layerB[scenario.mode] = {
|
|
158
|
+
crashExitCode: crash.code,
|
|
159
|
+
recoverAll: result.recoverAll,
|
|
160
|
+
lastAnalyzedCommit: result.lastAnalyzedCommit,
|
|
161
|
+
activeClaimRunId: result.activeClaimRunId,
|
|
162
|
+
readmeStartsWith: readme.slice(0, 40),
|
|
163
|
+
changesFiles: result.changesFiles,
|
|
164
|
+
indexDirty: result.indexDirty,
|
|
165
|
+
indexGeneration: result.indexGeneration,
|
|
166
|
+
secondRecoverAll: againResult.recoverAll,
|
|
167
|
+
};
|
|
168
|
+
if (scenario.expect === 'rolled-back') {
|
|
169
|
+
assert.equal(result.recoverAll[0].phase, 'rolled-back', `${scenario.mode}: journal rolled back consistently`);
|
|
170
|
+
assert.ok(readme.startsWith('# eng009 fixture knowledge (original)'), `${scenario.mode}: knowledge restored - no half-promoted state`);
|
|
171
|
+
assert.deepEqual(result.changesFiles, [], `${scenario.mode}: created file removed by rollback`);
|
|
172
|
+
assert.equal(result.lastAnalyzedCommit, null, `${scenario.mode}: pointer NOT advanced`);
|
|
173
|
+
assert.equal(result.activeClaimRunId, 'run-11111111-2222-3333-4444-555555555555', `${scenario.mode}: claim retained for explicit retry, not silently faked`);
|
|
174
|
+
} else {
|
|
175
|
+
assert.equal(result.recoverAll[0].phase, 'completed', 'after-apply: journal completed forward');
|
|
176
|
+
assert.ok(readme.startsWith('# eng009 fixture knowledge (replaced)'), 'after-apply: both operations final');
|
|
177
|
+
assert.equal(result.changesFiles.length, 1, 'after-apply: created file present');
|
|
178
|
+
assert.equal(result.lastAnalyzedCommit, 'a'.repeat(40), 'after-apply: pointer advanced exactly once via recovery');
|
|
179
|
+
assert.equal(result.activeClaimRunId, null, 'after-apply: active claim cleared');
|
|
180
|
+
assert.equal(result.indexDirty, true, 'after-apply: index marked dirty for repair');
|
|
181
|
+
assert.equal(result.indexGeneration, 1, 'after-apply: index generation advanced once');
|
|
182
|
+
}
|
|
183
|
+
assert.ok(againResult.recoverAll.every(r => r.skipped === true || r.phase === 'rolled-back' || r.phase === 'completed'), 'second recovery is idempotent (skipped)');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
evidence.verdict = {
|
|
187
|
+
'expected1 journal恢复一致(终端相位+二次恢复幂等)': 'PASS',
|
|
188
|
+
'expected2 不留下半晋升的假成功(mid-apply回滚/after-apply完整推进/崩溃后重分析而非假成功)': 'PASS',
|
|
189
|
+
'expected3 pointer顺序正确(仅经journal完成推进, 基线→X单一序列)': 'PASS',
|
|
190
|
+
injectionPoints: 'Layer A: real process hard-kill mid-analysis; Layer B: vendored KnowledgePromotionService production fault hook (journal-prepared / operation-applied:0 / promotion-verified)',
|
|
191
|
+
analyzerStandin: 'declarative stand-in - NO real model output covered',
|
|
192
|
+
vectorDouble: 'loopback HTTP double - vector-hub itself not covered',
|
|
193
|
+
};
|
|
194
|
+
await vector.close();
|
|
195
|
+
saveEvidence(ARTIFACTS, 'eng009-evidence.json', evidence);
|
|
196
|
+
console.log('ENG-009 PASS');
|
|
197
|
+
process.exitCode = 0;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
evidence.verdict = { failed: String(error && error.stack || error) };
|
|
200
|
+
try { await engine1.stop(); } catch {}
|
|
201
|
+
try { await vector.close(); } catch {}
|
|
202
|
+
saveEvidence(ARTIFACTS, 'eng009-evidence.json', evidence);
|
|
203
|
+
console.error(`ENG-009 FAIL: ${error.message}`);
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main();
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// P-ENG-ADVANCED ENG-010 索引独立失败 (independent index failure) acceptance driver.
|
|
4
|
+
//
|
|
5
|
+
// Case (qa-autopilot/config/test-cases.json ENG-010, P1):
|
|
6
|
+
// steps: Markdown晋升成功后令向量服务失败。
|
|
7
|
+
// expected: 1) 知识已更新/索引待修复分开。
|
|
8
|
+
// 2) 不回滚Markdown、不重复付费分析。
|
|
9
|
+
//
|
|
10
|
+
// Real behavior under test: vendored knowledge-engine child process, real
|
|
11
|
+
// promotion to the project knowledge tree on disk, then the vector endpoint
|
|
12
|
+
// (loopback double scripted to HTTP 503 EMBEDDING_UNAVAILABLE) failing exactly
|
|
13
|
+
// at index time. Verified via coordinator.index() semantics (src/coordinator.js):
|
|
14
|
+
// promotedAt is persisted BEFORE index(); an index failure moves the run to
|
|
15
|
+
// state 'index-pending' with error INDEX_FAILED “知识已更新,索引待修复。”
|
|
16
|
+
// while the Markdown promotion stays on disk; retry()/restart re-enter
|
|
17
|
+
// execute()'s promotedAt fast-path (line 219) which only re-indexes.
|
|
18
|
+
//
|
|
19
|
+
// Analyzer = declarative stand-in (NO real model output). Vector endpoint =
|
|
20
|
+
// loopback double (vector-hub itself not covered).
|
|
21
|
+
|
|
22
|
+
const assert = require('node:assert/strict');
|
|
23
|
+
const crypto = require('node:crypto');
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const {
|
|
27
|
+
makeLaneDir, buildFixtureRepo, waitUntil, callJson, startVectorDouble,
|
|
28
|
+
startEngineChild, readEventsFile, writePlan, saveEvidence,
|
|
29
|
+
} = require('./helpers/parallel-engine-advanced-harness.js');
|
|
30
|
+
|
|
31
|
+
const ARTIFACTS = process.env.PK_TEST_EVIDENCE_ROOT
|
|
32
|
+
? path.join(process.env.PK_TEST_EVIDENCE_ROOT, 'eng010')
|
|
33
|
+
: path.resolve(__dirname, '../../qa-autopilot/artifacts/parallel-20261023/engine-review-r3/reruns/eng010');
|
|
34
|
+
const PROJECT_ID = 'project-eng010';
|
|
35
|
+
const VECTOR_FAILURE = { kind: 'fail', httpStatus: 503, code: 'EMBEDDING_UNAVAILABLE', message: 'embedding backend unavailable (scripted)' };
|
|
36
|
+
|
|
37
|
+
function hashFile(file) {
|
|
38
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readRunFromDisk(dataDir, runId) {
|
|
42
|
+
const state = JSON.parse(fs.readFileSync(path.join(dataDir, 'engine-state.json'), 'utf8'));
|
|
43
|
+
return { state, run: state.runs.find(r => r.runId === runId) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const lane = makeLaneDir('eng010');
|
|
48
|
+
const commands = [];
|
|
49
|
+
const evidence = { caseId: 'ENG-010', laneDir: lane, phases: {}, commands };
|
|
50
|
+
const fixture = buildFixtureRepo(lane, 'eng010', ['X', 'Y']);
|
|
51
|
+
const knowledgePath = path.join(lane, 'knowledge-eng010');
|
|
52
|
+
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
53
|
+
const planFile = path.join(lane, 'analyzer-plan.json');
|
|
54
|
+
const eventsFile = path.join(lane, 'analyzer-events.jsonl');
|
|
55
|
+
writePlan(planFile, { default: { delayMs: 300 }, rules: [] });
|
|
56
|
+
|
|
57
|
+
const vector = await startVectorDouble();
|
|
58
|
+
vector.setMode(VECTOR_FAILURE);
|
|
59
|
+
const dataDir = path.join(lane, 'engine-data');
|
|
60
|
+
const engine1 = await startEngineChild({ label: 'eng010', dataDir, vectorUrl: vector.url, planFile, eventsFile });
|
|
61
|
+
commands.push('POST /api/runs X while vector double scripted 503 EMBEDDING_UNAVAILABLE', 'POST /api/runs/X retry after vector double restored', 'stop engine, re-fail vector, POST /api/runs Y -> index-pending', 'restart engine with vector restored (initialize() auto-retry)');
|
|
62
|
+
|
|
63
|
+
const changesDir = path.join(knowledgePath, 'changes');
|
|
64
|
+
const fileFor = sha => path.join(changesDir, `${sha.slice(0, 12)}.md`);
|
|
65
|
+
const startsFor = sha => readEventsFile(eventsFile).filter(e => e.kind === 'start' && e.commitSha === sha).length;
|
|
66
|
+
const runById = async (engine, runId) => (await callJson(engine.base, `/api/runs?projectId=${PROJECT_ID}`)).body.runs.find(r => r.runId === runId);
|
|
67
|
+
const runtimeOf = async engine => (await callJson(engine.base, `/api/snapshot?projectId=${PROJECT_ID}`)).body.projects.find(p => p.projectId === PROJECT_ID).runtime;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
assert.equal((await callJson(engine1.base, '/api/projects', {
|
|
71
|
+
projectId: PROJECT_ID, name: 'eng010', repoPath: fixture.repoPath, knowledgePath, trackingStartCommit: fixture.baseline,
|
|
72
|
+
})).status, 200);
|
|
73
|
+
const runX = (await callJson(engine1.base, '/api/runs', { projectId: PROJECT_ID, commitSha: fixture.shas.X })).body.run;
|
|
74
|
+
|
|
75
|
+
// --- Expected 1: 知识已更新 / 索引待修复 分开 ---
|
|
76
|
+
const stuck = await waitUntil(async () => {
|
|
77
|
+
const run = await runById(engine1, runX.runId);
|
|
78
|
+
return run?.state === 'index-pending' ? run : null;
|
|
79
|
+
}, 30000, 'run reaches index-pending (promotion done, index failed)');
|
|
80
|
+
const runtime1 = await runtimeOf(engine1);
|
|
81
|
+
const fileX = fileFor(fixture.shas.X);
|
|
82
|
+
const hashX = hashFile(fileX);
|
|
83
|
+
const diskX = readRunFromDisk(dataDir, runX.runId);
|
|
84
|
+
evidence.phases.indexFailure = {
|
|
85
|
+
run: { state: stuck.state, phase: stuck.phase, indexState: stuck.indexState, promotedAt: stuck.promotedAt, error: stuck.error },
|
|
86
|
+
runtime: { lastAnalyzedCommit: runtime1.lastAnalyzedCommit, indexDirty: runtime1.index.dirty, indexStatus: runtime1.index.status },
|
|
87
|
+
knowledgeFile: { path: path.basename(fileX), bytes: fs.statSync(fileX).size, hash: hashX },
|
|
88
|
+
analyzerStartsForX: startsFor(fixture.shas.X),
|
|
89
|
+
vectorSyncRequests: vector.requests.filter(r => r.url === '/api/sync').length,
|
|
90
|
+
diskIndexAttempts: diskX.run.indexAttempts,
|
|
91
|
+
diskState: diskX.run.state,
|
|
92
|
+
};
|
|
93
|
+
assert.equal(stuck.state, 'index-pending', 'index failure is a SEPARATE state, not failed');
|
|
94
|
+
assert.equal(stuck.phase, 'index-pending');
|
|
95
|
+
assert.equal(stuck.indexState, 'failed');
|
|
96
|
+
assert.ok(stuck.promotedAt, 'promotion timestamp recorded - knowledge IS updated');
|
|
97
|
+
assert.equal(stuck.error.code, 'INDEX_FAILED');
|
|
98
|
+
assert.equal(stuck.error.message, '知识已更新,索引待修复。');
|
|
99
|
+
assert.equal(stuck.error.retryable, true);
|
|
100
|
+
assert.equal(stuck.error.details.failureKind, 'HTTP_ERROR');
|
|
101
|
+
assert.equal(stuck.error.details.httpStatus, 503);
|
|
102
|
+
assert.equal(stuck.error.details.upstreamCode, 'EMBEDDING_UNAVAILABLE');
|
|
103
|
+
assert.equal(stuck.error.details.attempt, 1);
|
|
104
|
+
assert.equal(runtime1.lastAnalyzedCommit, fixture.shas.X, 'pointer advanced - Markdown promotion committed');
|
|
105
|
+
assert.equal(runtime1.index.dirty, true, 'runtime index marked dirty while knowledge is current');
|
|
106
|
+
assert.ok(fs.existsSync(fileX), 'promoted Markdown exists on disk');
|
|
107
|
+
assert.ok(fs.statSync(fileX).size > 0, 'promoted Markdown has real bytes');
|
|
108
|
+
assert.equal(startsFor(fixture.shas.X), 1, 'analyzer ran exactly once');
|
|
109
|
+
assert.equal(diskX.run.indexAttempts, 1, 'disk state records exactly one index attempt');
|
|
110
|
+
|
|
111
|
+
// --- Expected 2 (part 1): explicit retry repairs the index only ---
|
|
112
|
+
const promotedAtX = stuck.promotedAt;
|
|
113
|
+
vector.setMode({ kind: 'ok' });
|
|
114
|
+
const retried = await callJson(engine1.base, `/api/runs/${encodeURIComponent(runX.runId)}/retry`, {});
|
|
115
|
+
assert.equal(retried.status, 200, `retry failed: ${JSON.stringify(retried.body)}`);
|
|
116
|
+
const healed = await waitUntil(async () => {
|
|
117
|
+
const run = await runById(engine1, runX.runId);
|
|
118
|
+
return run?.state === 'succeeded' ? run : null;
|
|
119
|
+
}, 30000, 'retried run succeeds after vector restored');
|
|
120
|
+
const runtime2 = await runtimeOf(engine1);
|
|
121
|
+
const diskX2 = readRunFromDisk(dataDir, runX.runId);
|
|
122
|
+
evidence.phases.explicitRetry = {
|
|
123
|
+
run: { state: healed.state, phase: healed.phase, indexState: healed.indexState, promotedAt: healed.promotedAt, error: healed.error },
|
|
124
|
+
promotedAtUnchanged: healed.promotedAt === promotedAtX,
|
|
125
|
+
knowledgeFileHashAfterRetry: hashFile(fileX),
|
|
126
|
+
knowledgeFileHashBefore: hashX,
|
|
127
|
+
analyzerStartsForX: startsFor(fixture.shas.X),
|
|
128
|
+
diskIndexAttempts: diskX2.run.indexAttempts,
|
|
129
|
+
vectorSyncRequests: vector.requests.filter(r => r.url === '/api/sync').length,
|
|
130
|
+
runtime: { indexDirty: runtime2.index.dirty, indexStatus: runtime2.index.status },
|
|
131
|
+
};
|
|
132
|
+
assert.equal(healed.phase, 'ready');
|
|
133
|
+
assert.equal(healed.indexState, 'succeeded');
|
|
134
|
+
assert.equal(healed.error, null);
|
|
135
|
+
assert.equal(healed.promotedAt, promotedAtX, 'promotion timestamp untouched by index retry');
|
|
136
|
+
assert.equal(hashFile(fileX), hashX, 'Markdown bytes NOT rolled back or rewritten by index retry');
|
|
137
|
+
assert.equal(startsFor(fixture.shas.X), 1, 'no second analyzer run - no duplicate paid analysis');
|
|
138
|
+
assert.equal(diskX2.run.indexAttempts, 2, 'second index attempt recorded on disk');
|
|
139
|
+
assert.equal(vector.requests.filter(r => r.url === '/api/sync').length, 2, 'one failed + one successful sync request');
|
|
140
|
+
assert.equal(runtime2.index.dirty, false, 'index repaired: dirty cleared');
|
|
141
|
+
assert.equal(runtime2.index.status, 'ready');
|
|
142
|
+
|
|
143
|
+
// --- Expected 2 (part 2): restart auto-repairs index-pending without re-analysis ---
|
|
144
|
+
vector.setMode(VECTOR_FAILURE);
|
|
145
|
+
const runY = (await callJson(engine1.base, '/api/runs', { projectId: PROJECT_ID, commitSha: fixture.shas.Y })).body.run;
|
|
146
|
+
const stuckY = await waitUntil(async () => {
|
|
147
|
+
const run = await runById(engine1, runY.runId);
|
|
148
|
+
return run?.state === 'index-pending' ? run : null;
|
|
149
|
+
}, 30000, 'second run also reaches index-pending');
|
|
150
|
+
const promotedAtY = stuckY.promotedAt;
|
|
151
|
+
const fileY = fileFor(fixture.shas.Y);
|
|
152
|
+
const hashY = hashFile(fileY);
|
|
153
|
+
const startsYBeforeRestart = startsFor(fixture.shas.Y);
|
|
154
|
+
await engine1.stop();
|
|
155
|
+
const diskY = readRunFromDisk(dataDir, runY.runId);
|
|
156
|
+
evidence.phases.restartSetup = {
|
|
157
|
+
runY: { state: diskY.run.state, phase: diskY.run.phase, promotedAt: diskY.run.promotedAt, diskIndexAttempts: diskY.run.indexAttempts },
|
|
158
|
+
knowledgeFileY: { hash: hashY, analyzerStarts: startsYBeforeRestart },
|
|
159
|
+
};
|
|
160
|
+
assert.equal(diskY.run.state, 'index-pending', 'index-pending persisted across shutdown');
|
|
161
|
+
|
|
162
|
+
vector.setMode({ kind: 'ok' });
|
|
163
|
+
const engine2 = await startEngineChild({ label: 'eng010-restart', dataDir, vectorUrl: vector.url, planFile, eventsFile });
|
|
164
|
+
try {
|
|
165
|
+
const autoHealed = await waitUntil(async () => {
|
|
166
|
+
const run = await runById(engine2, runY.runId);
|
|
167
|
+
return run?.state === 'succeeded' ? run : null;
|
|
168
|
+
}, 45000, 'restart auto-retries index-pending run at initialize()');
|
|
169
|
+
const diskY2 = readRunFromDisk(dataDir, runY.runId);
|
|
170
|
+
const runtime3 = await runtimeOf(engine2);
|
|
171
|
+
evidence.phases.restartAutoRetry = {
|
|
172
|
+
runY: { state: autoHealed.state, phase: autoHealed.phase, indexState: autoHealed.indexState, promotedAt: autoHealed.promotedAt, error: autoHealed.error },
|
|
173
|
+
promotedAtUnchanged: autoHealed.promotedAt === promotedAtY,
|
|
174
|
+
knowledgeFileYHashAfterRestart: hashFile(fileY),
|
|
175
|
+
knowledgeFileYHashBefore: hashY,
|
|
176
|
+
analyzerStartsForYTotal: startsFor(fixture.shas.Y),
|
|
177
|
+
diskIndexAttempts: diskY2.run.indexAttempts,
|
|
178
|
+
runtime: { indexDirty: runtime3.index.dirty, indexStatus: runtime3.index.status },
|
|
179
|
+
syncsForY: vector.requests.filter(r => r.url === '/api/sync' && r.body.project === PROJECT_ID).length,
|
|
180
|
+
};
|
|
181
|
+
assert.equal(autoHealed.promotedAt, promotedAtY, 'restart did not re-promote');
|
|
182
|
+
assert.equal(hashFile(fileY), hashY, 'Markdown bytes unchanged across restart auto-repair');
|
|
183
|
+
assert.equal(startsFor(fixture.shas.Y), startsYBeforeRestart, 'restart auto-repair did NOT run the analyzer again');
|
|
184
|
+
assert.equal(diskY2.run.indexAttempts, 2, 'restart performed exactly one more index attempt');
|
|
185
|
+
assert.equal(runtime3.index.dirty, false, 'index clean after auto-repair');
|
|
186
|
+
await engine2.stop();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
try { await engine2.stop(); } catch {}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
evidence.verdict = {
|
|
193
|
+
'expected1 知识已更新/索引待修复分开(index-pending独立状态+INDEX_FAILED可重试+指针已推进+dirty标记)': 'PASS',
|
|
194
|
+
'expected2 不回滚Markdown、不重复付费分析(字节hash不变/analyzer恰好1次/promotedAt不变/重试只补索引)': 'PASS',
|
|
195
|
+
extraRestartAutoRetry: 'PASS - initialize() automatically retries persisted index-pending without re-analysis',
|
|
196
|
+
analyzerStandin: 'declarative stand-in - NO real model output covered',
|
|
197
|
+
vectorDouble: 'loopback HTTP double scripted 503 EMBEDDING_UNAVAILABLE - vector-hub itself not covered',
|
|
198
|
+
};
|
|
199
|
+
await vector.close();
|
|
200
|
+
saveEvidence(ARTIFACTS, 'eng010-evidence.json', evidence);
|
|
201
|
+
console.log('ENG-010 PASS');
|
|
202
|
+
process.exitCode = 0;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
evidence.verdict = { failed: String(error && error.stack || error) };
|
|
205
|
+
try { await engine1.stop(); } catch {}
|
|
206
|
+
try { await vector.close(); } catch {}
|
|
207
|
+
saveEvidence(ARTIFACTS, 'eng010-evidence.json', evidence);
|
|
208
|
+
console.error(`ENG-010 FAIL: ${error.message}`);
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main();
|