@sanqianx/project-knowledge 5.0.19 → 5.0.20-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.agents/plugins/marketplace.json +3 -3
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/CHANGELOG.md +5 -0
  4. package/LICENSE +201 -201
  5. package/NOTICE +5 -5
  6. package/README.md +349 -349
  7. package/_modules/knowledge-engine/README.md +4 -0
  8. package/_modules/knowledge-engine/src/analysis-failures.js +39 -0
  9. package/_modules/knowledge-engine/src/analysis-worker.js +6 -2
  10. package/_modules/knowledge-engine/src/coordinator.js +7 -0
  11. package/_modules/knowledge-engine/src/headless-analyzer.js +2 -1
  12. package/_modules/knowledge-engine/src/server.js +1 -1
  13. package/_site/_test/agent-capture-mcp-ui-test.js +67 -67
  14. package/_site/_test/ai-profile-test.js +75 -75
  15. package/_site/_test/ai-vendor-presets-test.js +91 -91
  16. package/_site/_test/automatic-context-test.js +70 -70
  17. package/_site/_test/bin-cli-test.js +230 -230
  18. package/_site/_test/bridge-adapter-test.js +95 -95
  19. package/_site/_test/bridge-consumer-service-test.js +310 -310
  20. package/_site/_test/candidate-bin-content-test.js +35 -35
  21. package/_site/_test/candidate-product-assets-test.js +13 -13
  22. package/_site/_test/candidate-release-contract-test.js +14 -14
  23. package/_site/_test/capture-hook-dedup-test.js +158 -158
  24. package/_site/_test/claude-executable-discovery-test.js +28 -28
  25. package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -252
  26. package/_site/_test/cli-instance-management-test.js +62 -62
  27. package/_site/_test/client-capture-restart-e2e-test.js +189 -189
  28. package/_site/_test/codex-conversation-projection-test.js +60 -60
  29. package/_site/_test/codex-marketplace-scoped-path-test.js +186 -186
  30. package/_site/_test/codex-notify-capture-e2e-test.js +101 -101
  31. package/_site/_test/codex-notify-fanout-test.js +134 -134
  32. package/_site/_test/codex-notify-upgrade-test.js +253 -253
  33. package/_site/_test/commit-boundary-freeze-test.js +147 -147
  34. package/_site/_test/commit-conversation-binding-test.js +4 -4
  35. package/_site/_test/configuration-generation-test.js +22 -22
  36. package/_site/_test/context-pack-test.js +200 -200
  37. package/_site/_test/conversation-api-test.js +10 -10
  38. package/_site/_test/conversation-origin-test.js +15 -15
  39. package/_site/_test/cross-repo-isolation-e2e-test.js +187 -187
  40. package/_site/_test/embedding-config-test.js +32 -32
  41. package/_site/_test/fixtures/fake-browser-exit.js +10 -10
  42. package/_site/_test/fixtures/fake-browser-hang.js +10 -10
  43. package/_site/_test/fixtures/integration-busy-worker.cjs +6 -6
  44. package/_site/_test/fixtures/make-git-repos.js +92 -92
  45. package/_site/_test/fixtures/retrieval-gold-queries.json +32 -32
  46. package/_site/_test/fresh-hook-reimport-test.js +53 -53
  47. package/_site/_test/fresh-product-data-test.js +56 -56
  48. package/_site/_test/git-path-compatibility-test.js +80 -80
  49. package/_site/_test/git-validation-test.js +81 -81
  50. package/_site/_test/hook-worktree-test.js +42 -42
  51. package/_site/_test/index-builder-test.js +41 -41
  52. package/_site/_test/index-error-display-contract-test.js +10 -10
  53. package/_site/_test/integration-adapters-test.js +67 -67
  54. package/_site/_test/integration-capture-setup-test.js +118 -118
  55. package/_site/_test/integration-runtime-binding-a2-test.js +566 -566
  56. package/_site/_test/integration-runtime-binding-a3-test.js +227 -227
  57. package/_site/_test/integration-runtime-binding-test.js +473 -473
  58. package/_site/_test/integration-status-scan-test.js +29 -29
  59. package/_site/_test/kb-framework-test.js +57 -57
  60. package/_site/_test/kbpath-follow-test.js +63 -63
  61. package/_site/_test/knowledge-db-test.js +144 -144
  62. package/_site/_test/knowledge-engine-client-test.js +109 -109
  63. package/_site/_test/knowledge-maintenance-test.js +40 -40
  64. package/_site/_test/knowledge-panel-ui-contract-test.js +112 -0
  65. package/_site/_test/knowledge-scopes-test.js +37 -37
  66. package/_site/_test/knowledge-storage-location-test.js +28 -28
  67. package/_site/_test/launcher-readiness-test.js +80 -80
  68. package/_site/_test/legacy-claude-session-alias-test.js +106 -106
  69. package/_site/_test/legacy-routes-removed-test.js +47 -47
  70. package/_site/_test/legacy-workbench-exclusion-test.js +92 -92
  71. package/_site/_test/live-gateway-test.js +48 -48
  72. package/_site/_test/local-marketplace-test.js +16 -16
  73. package/_site/_test/log-redaction-test.js +58 -58
  74. package/_site/_test/markdown-maintenance-test.js +11 -11
  75. package/_site/_test/model-context-windows-test.js +89 -89
  76. package/_site/_test/module-agent-start-proxy-test.js +65 -65
  77. package/_site/_test/module-artifact-integrity-test.js +27 -27
  78. package/_site/_test/module-boundary-test.js +30 -30
  79. package/_site/_test/module-bridge-eventbridge-test.js +202 -202
  80. package/_site/_test/module-dynamic-port-test.js +118 -118
  81. package/_site/_test/module-index-proxy-test.js +33 -33
  82. package/_site/_test/module-model-configuration-test.js +44 -44
  83. package/_site/_test/module-process-lifecycle-test.js +35 -35
  84. package/_site/_test/module-stream-proxy-test.js +34 -34
  85. package/_site/_test/module-terminal-restart-test.js +63 -63
  86. package/_site/_test/module-wiring-unbind-race-test.js +59 -59
  87. package/_site/_test/native-context-integrations-test.js +86 -86
  88. package/_site/_test/orphaned-claim-recovery-test.js +80 -80
  89. package/_site/_test/package-boundary-test.js +43 -42
  90. package/_site/_test/package-startup-test.js +56 -56
  91. package/_site/_test/packaged-ui-smoke-test.js +88 -88
  92. package/_site/_test/pr-consumer-contract-test.js +55 -55
  93. package/_site/_test/product-brand-contract-test.js +25 -25
  94. package/_site/_test/product-diagnostics-ui-test.js +93 -93
  95. package/_site/_test/product-import-ui-test.js +104 -104
  96. package/_site/_test/product-progress-status-test.js +12 -1
  97. package/_site/_test/project-remove-running-guard-test.js +79 -79
  98. package/_site/_test/project-store-test.js +90 -90
  99. package/_site/_test/registry-events-test.js +38 -38
  100. package/_site/_test/release-version-sync-test.js +115 -115
  101. package/_site/_test/requirement-binding-test.js +113 -113
  102. package/_site/_test/runtime-endpoint-test.js +64 -64
  103. package/_site/_test/scanner-test.js +83 -83
  104. package/_site/_test/server-security-test.js +96 -96
  105. package/_site/_test/sessions-stream-test.js +93 -93
  106. package/_site/_test/shared-scope-root-binding-test.js +67 -67
  107. package/_site/_test/simple-import-test.js +98 -98
  108. package/_site/_test/startup-analysis-disabled-test.js +6 -6
  109. package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -196
  110. package/_site/_test/team-knowledge-store-test.js +132 -132
  111. package/_site/_test/test-server-cleanup-test.js +35 -35
  112. package/_site/_test/test-server-module-isolation-test.js +37 -37
  113. package/_site/_test/tracking-start-test.js +35 -35
  114. package/_site/_test/workbench-context-receipts-test.js +22 -22
  115. package/_site/_test/workspace-ui-contract-test.js +31 -31
  116. package/_site/_test/zcode-capture-integration-test.js +108 -108
  117. package/_site/lib/ai-vendor-presets.js +83 -83
  118. package/_site/lib/ai-workspace.js +3 -3
  119. package/_site/lib/atomic-file.js +204 -204
  120. package/_site/lib/bridge-adapter.js +260 -260
  121. package/_site/lib/capture-hook-config.js +108 -108
  122. package/_site/lib/claude-cli-runner.js +89 -89
  123. package/_site/lib/cli-instances.js +43 -43
  124. package/_site/lib/codex-context-status.js +17 -17
  125. package/_site/lib/codex-notify-config.js +193 -193
  126. package/_site/lib/commit-conversation-binder.js +12 -12
  127. package/_site/lib/commit-reconciler.js +38 -38
  128. package/_site/lib/context-pack-builder.js +299 -299
  129. package/_site/lib/conversation-exclusions.js +204 -204
  130. package/_site/lib/conversation-query-service.js +17 -17
  131. package/_site/lib/data-dir.js +13 -13
  132. package/_site/lib/embedding-service.js +85 -85
  133. package/_site/lib/engine-connection.js +93 -93
  134. package/_site/lib/engine-knowledge-client.js +72 -72
  135. package/_site/lib/folder-picker-output.js +3 -3
  136. package/_site/lib/git-path.js +14 -14
  137. package/_site/lib/github-team-store.js +3 -3
  138. package/_site/lib/index-builder.js +157 -157
  139. package/_site/lib/integration-status-scan.js +99 -99
  140. package/_site/lib/integration-status-worker.js +14 -14
  141. package/_site/lib/kb-framework.js +15 -15
  142. package/_site/lib/knowledge-db.js +363 -363
  143. package/_site/lib/knowledge-schema.js +121 -121
  144. package/_site/lib/live-gateway.js +78 -78
  145. package/_site/lib/llm-client.js +12 -12
  146. package/_site/lib/model-context-windows.js +82 -82
  147. package/_site/lib/module-bridge.js +874 -874
  148. package/_site/lib/module-port-resolver.js +57 -57
  149. package/_site/lib/post-commit-automation.js +54 -54
  150. package/_site/lib/product-data.js +35 -35
  151. package/_site/lib/project-lifecycle-service.js +14 -14
  152. package/_site/lib/project-registry-store.js +129 -129
  153. package/_site/lib/prompt-registry.js +148 -148
  154. package/_site/lib/registry-events.js +16 -16
  155. package/_site/lib/requirement-adapters.js +10 -10
  156. package/_site/lib/requirement-binder.js +112 -112
  157. package/_site/lib/runtime-control.js +89 -89
  158. package/_site/lib/runtime-endpoint.js +5 -5
  159. package/_site/lib/zcode-context-integration.js +60 -60
  160. package/_site/scripts/codex-notify-fanout.cjs +95 -95
  161. package/_site/scripts/install-module-candidates.js +69 -69
  162. package/_site/scripts/pack-module-candidates.js +89 -89
  163. package/_site/scripts/sync-product-content.js +28 -28
  164. package/_site/scripts/sync-release-version.js +164 -164
  165. package/_site/scripts/vendor-modules.js +49 -49
  166. package/_site/scripts/verify-module-runtime.js +50 -50
  167. package/_site/scripts/verify-product-runtime.js +36 -36
  168. package/_site/scripts/verify-retrieval-gold.js +83 -83
  169. package/_site/server.js +15 -15
  170. package/_site/start.bat +26 -26
  171. package/_site/stop.bat +11 -11
  172. package/bin/project-knowledge-integrations.js +254 -254
  173. package/bin/project-knowledge-kb.js +67 -67
  174. package/bin/project-knowledge-mcp.js +8 -8
  175. package/bin/project-knowledge.js +170 -170
  176. package/claude-prompts.json +21 -21
  177. package/docs/README.zh-CN.md +205 -205
  178. package/docs/assets/architecture.svg +181 -181
  179. package/docs/assets/logo.svg +49 -49
  180. package/docs/fresh-v5-start.md +42 -42
  181. package/docs/leader-glm-workbench-playbook.md +122 -122
  182. package/docs/project-registry-schema.md +11 -11
  183. package/docs/release-5.0.16-acceptance.md +176 -176
  184. package/docs/release-5.0.17-acceptance.md +117 -117
  185. package/docs/release-5.0.19-acceptance.md +39 -25
  186. package/docs/team-knowledge-mode-a-plan.md +73 -73
  187. package/module-runtime-manifest.json +53 -33
  188. package/package.json +3 -2
  189. package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
  190. package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
  191. package/plugins/project-knowledge/.mcp.json +1 -1
  192. package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
  193. package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
  194. package/templates/change.md +30 -30
  195. package/templates/project-readme.md +18 -18
  196. package/ui/index.html +1 -1
  197. package/ui/knowledge-panel.html +118 -0
  198. package/ui/product.css +8 -0
  199. package/ui/product.js +146 -4
  200. package/vendor-manifest.json +55 -54
@@ -1,29 +1,29 @@
1
- 'use strict';
2
- const assert = require('node:assert/strict');
3
- const path = require('node:path');
4
- const { IntegrationStatusScan } = require('../lib/integration-status-scan');
5
- async function main() {
6
- const scan = new IntegrationStatusScan({ workerFile: path.join(__dirname, 'fixtures/integration-busy-worker.cjs'), timeoutMs: 5000 });
7
- try {
8
- const before = Date.now();
9
- const first = await scan.read({ rootDir: path.resolve(__dirname, '../..') }, { clients: ['claude'] }, { waitMs: 50 });
10
- assert.equal(first.scan.state, 'checking'); assert.ok(Date.now() - before < 500);
11
- const again = await scan.read({ rootDir: path.resolve(__dirname, '../..') }, { clients: ['claude'] }, { waitMs: 10 });
12
- assert.equal(first.scan.generation, again.scan.generation); assert.equal(scan.children.size, 1);
13
- let ticked = false; await new Promise(resolve => setTimeout(() => { ticked = true; resolve(); }, 20));
14
- assert.equal(ticked, true, 'a blocked probe cannot freeze the HTTP event loop');
15
- await new Promise(resolve => setTimeout(resolve, 1500));
16
- const ready = await scan.read({ rootDir: path.resolve(__dirname, '../..') }, { clients: ['claude'] });
17
- assert.equal(ready.scan.state, 'ready'); assert.equal(ready.clients[0].available, true);
18
- console.log('Isolated status scan, coalescing and nonblocking snapshot passed.');
19
- } finally { scan.close(); }
20
- const logs = [], timed = new IntegrationStatusScan({ workerFile: path.join(__dirname, 'fixtures/integration-busy-worker.cjs'), timeoutMs: 100 });
21
- try {
22
- const result = await timed.read({ rootDir: path.resolve(__dirname, '../..'), operationId: 'status-timeout-test', logger: { warn: (...args) => logs.push(args) } }, { clients: ['claude'] }, { waitMs: 500 });
23
- assert.equal(result.error.code, 'INTEGRATION_STATUS_TIMEOUT');
24
- assert.equal(result.clients[0].automaticContext.state, 'check-failed');
25
- assert.equal(logs[0][0], 'integration.status.timeout');
26
- assert.equal(logs[0][2].operationId, 'status-timeout-test');
27
- } finally { timed.close(); }
28
- }
29
- main().catch(error => { console.error(error); process.exitCode = 1; });
1
+ 'use strict';
2
+ const assert = require('node:assert/strict');
3
+ const path = require('node:path');
4
+ const { IntegrationStatusScan } = require('../lib/integration-status-scan');
5
+ async function main() {
6
+ const scan = new IntegrationStatusScan({ workerFile: path.join(__dirname, 'fixtures/integration-busy-worker.cjs'), timeoutMs: 5000 });
7
+ try {
8
+ const before = Date.now();
9
+ const first = await scan.read({ rootDir: path.resolve(__dirname, '../..') }, { clients: ['claude'] }, { waitMs: 50 });
10
+ assert.equal(first.scan.state, 'checking'); assert.ok(Date.now() - before < 500);
11
+ const again = await scan.read({ rootDir: path.resolve(__dirname, '../..') }, { clients: ['claude'] }, { waitMs: 10 });
12
+ assert.equal(first.scan.generation, again.scan.generation); assert.equal(scan.children.size, 1);
13
+ let ticked = false; await new Promise(resolve => setTimeout(() => { ticked = true; resolve(); }, 20));
14
+ assert.equal(ticked, true, 'a blocked probe cannot freeze the HTTP event loop');
15
+ await new Promise(resolve => setTimeout(resolve, 1500));
16
+ const ready = await scan.read({ rootDir: path.resolve(__dirname, '../..') }, { clients: ['claude'] });
17
+ assert.equal(ready.scan.state, 'ready'); assert.equal(ready.clients[0].available, true);
18
+ console.log('Isolated status scan, coalescing and nonblocking snapshot passed.');
19
+ } finally { scan.close(); }
20
+ const logs = [], timed = new IntegrationStatusScan({ workerFile: path.join(__dirname, 'fixtures/integration-busy-worker.cjs'), timeoutMs: 100 });
21
+ try {
22
+ const result = await timed.read({ rootDir: path.resolve(__dirname, '../..'), operationId: 'status-timeout-test', logger: { warn: (...args) => logs.push(args) } }, { clients: ['claude'] }, { waitMs: 500 });
23
+ assert.equal(result.error.code, 'INTEGRATION_STATUS_TIMEOUT');
24
+ assert.equal(result.clients[0].automaticContext.state, 'check-failed');
25
+ assert.equal(logs[0][0], 'integration.status.timeout');
26
+ assert.equal(logs[0][2].operationId, 'status-timeout-test');
27
+ } finally { timed.close(); }
28
+ }
29
+ main().catch(error => { console.error(error); process.exitCode = 1; });
@@ -1,57 +1,57 @@
1
- // Run: node _site/_test/kb-framework-test.js
2
-
3
- const assert = require('assert');
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
-
8
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-framework-${process.pid}-`));
9
- process.env.KB_DATA_DIR = path.join(temp, 'data');
10
- process.env.KB_SKIP_MIGRATION = '1';
11
- require('../lib/data-dir')._resetCache();
12
- const framework = require('../lib/kb-framework');
13
-
14
- (() => {
15
- try {
16
- const fresh = path.join(temp, 'knowledge', 'fresh');
17
- const initialized = framework.initProjectDirs('fresh-project', fresh);
18
- assert.strictEqual(initialized.lazy, true);
19
- assert.deepStrictEqual(initialized.created, []);
20
- assert.deepStrictEqual(fs.readdirSync(fresh), [], 'fresh import must not create guessed/TODO Markdown or empty collection indexes');
21
- const second = framework.initProjectDirs('fresh-project', fresh);
22
- assert.deepStrictEqual(second.created, [], 'lazy directory declaration should be idempotent');
23
- assert.strictEqual(fs.existsSync(path.join(process.env.KB_DATA_DIR, '_ai', 'fresh-project')), false, 'unused AI workspace must not be created');
24
-
25
- const legacy = path.join(temp, 'knowledge', 'legacy');
26
- fs.mkdirSync(path.join(legacy, '_ai', 'runs'), { recursive: true });
27
- fs.mkdirSync(path.join(legacy, 'commits'), { recursive: true });
28
- fs.mkdirSync(path.join(legacy, 'features'), { recursive: true });
29
- fs.writeFileSync(path.join(legacy, 'README.md'), '# Legacy Project\n\nVerified legacy overview.\n', 'utf8');
30
- fs.writeFileSync(path.join(legacy, 'project-goal.md'), '# Legacy Goal\n\nA verified migrated goal.\n', 'utf8');
31
- fs.writeFileSync(path.join(legacy, 'project-analysis.md'), '# Legacy Analysis\n\nA verified architecture fact.\n', 'utf8');
32
- fs.writeFileSync(path.join(legacy, 'commits', 'old.md'), '# Old Commit\n\nVerified old change.\n', 'utf8');
33
- fs.writeFileSync(path.join(legacy, '_ai', 'runs', 'old.json'), '{"runId":"old"}\n', 'utf8');
34
- const migrated = framework.migrateToFramework({ slug: 'legacy-project', kbPath: legacy });
35
- assert.strictEqual(migrated.ok, true);
36
- assert(fs.existsSync(path.join(legacy, 'GOAL.md')));
37
- assert(fs.existsSync(path.join(legacy, 'ARCHITECTURE.md')));
38
- assert(fs.existsSync(path.join(legacy, 'changes', 'legacy-change-01.md')));
39
- assert(fs.existsSync(path.join(legacy, 'changes', '00-index.md')));
40
- assert(fs.existsSync(path.join(legacy, 'modules', '00-index.md')));
41
- assert.strictEqual(fs.existsSync(path.join(legacy, '_ai')), false, 'internal AI data must be removed from user knowledge root after migration');
42
- assert(fs.existsSync(path.join(process.env.KB_DATA_DIR, '_ai', 'legacy-project', 'runs', 'old.json')), 'legacy AI run should be preserved internally');
43
- const allMarkdown = [];
44
- const walk = current => {
45
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
46
- const target = path.join(current, entry.name);
47
- if (entry.isDirectory()) walk(target);
48
- else if (entry.name.endsWith('.md')) allMarkdown.push(fs.readFileSync(target, 'utf8'));
49
- }
50
- };
51
- walk(legacy);
52
- assert(!allMarkdown.some(text => /TODO: confirm|TODO: summarize/.test(text)), 'migration must not introduce TODO facts');
53
- console.log('kb-framework-test PASS');
54
- } finally {
55
- fs.rmSync(temp, { recursive: true, force: true });
56
- }
57
- })();
1
+ // Run: node _site/_test/kb-framework-test.js
2
+
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), `kb-framework-${process.pid}-`));
9
+ process.env.KB_DATA_DIR = path.join(temp, 'data');
10
+ process.env.KB_SKIP_MIGRATION = '1';
11
+ require('../lib/data-dir')._resetCache();
12
+ const framework = require('../lib/kb-framework');
13
+
14
+ (() => {
15
+ try {
16
+ const fresh = path.join(temp, 'knowledge', 'fresh');
17
+ const initialized = framework.initProjectDirs('fresh-project', fresh);
18
+ assert.strictEqual(initialized.lazy, true);
19
+ assert.deepStrictEqual(initialized.created, []);
20
+ assert.deepStrictEqual(fs.readdirSync(fresh), [], 'fresh import must not create guessed/TODO Markdown or empty collection indexes');
21
+ const second = framework.initProjectDirs('fresh-project', fresh);
22
+ assert.deepStrictEqual(second.created, [], 'lazy directory declaration should be idempotent');
23
+ assert.strictEqual(fs.existsSync(path.join(process.env.KB_DATA_DIR, '_ai', 'fresh-project')), false, 'unused AI workspace must not be created');
24
+
25
+ const legacy = path.join(temp, 'knowledge', 'legacy');
26
+ fs.mkdirSync(path.join(legacy, '_ai', 'runs'), { recursive: true });
27
+ fs.mkdirSync(path.join(legacy, 'commits'), { recursive: true });
28
+ fs.mkdirSync(path.join(legacy, 'features'), { recursive: true });
29
+ fs.writeFileSync(path.join(legacy, 'README.md'), '# Legacy Project\n\nVerified legacy overview.\n', 'utf8');
30
+ fs.writeFileSync(path.join(legacy, 'project-goal.md'), '# Legacy Goal\n\nA verified migrated goal.\n', 'utf8');
31
+ fs.writeFileSync(path.join(legacy, 'project-analysis.md'), '# Legacy Analysis\n\nA verified architecture fact.\n', 'utf8');
32
+ fs.writeFileSync(path.join(legacy, 'commits', 'old.md'), '# Old Commit\n\nVerified old change.\n', 'utf8');
33
+ fs.writeFileSync(path.join(legacy, '_ai', 'runs', 'old.json'), '{"runId":"old"}\n', 'utf8');
34
+ const migrated = framework.migrateToFramework({ slug: 'legacy-project', kbPath: legacy });
35
+ assert.strictEqual(migrated.ok, true);
36
+ assert(fs.existsSync(path.join(legacy, 'GOAL.md')));
37
+ assert(fs.existsSync(path.join(legacy, 'ARCHITECTURE.md')));
38
+ assert(fs.existsSync(path.join(legacy, 'changes', 'legacy-change-01.md')));
39
+ assert(fs.existsSync(path.join(legacy, 'changes', '00-index.md')));
40
+ assert(fs.existsSync(path.join(legacy, 'modules', '00-index.md')));
41
+ assert.strictEqual(fs.existsSync(path.join(legacy, '_ai')), false, 'internal AI data must be removed from user knowledge root after migration');
42
+ assert(fs.existsSync(path.join(process.env.KB_DATA_DIR, '_ai', 'legacy-project', 'runs', 'old.json')), 'legacy AI run should be preserved internally');
43
+ const allMarkdown = [];
44
+ const walk = current => {
45
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
46
+ const target = path.join(current, entry.name);
47
+ if (entry.isDirectory()) walk(target);
48
+ else if (entry.name.endsWith('.md')) allMarkdown.push(fs.readFileSync(target, 'utf8'));
49
+ }
50
+ };
51
+ walk(legacy);
52
+ assert(!allMarkdown.some(text => /TODO: confirm|TODO: summarize/.test(text)), 'migration must not introduce TODO facts');
53
+ console.log('kb-framework-test PASS');
54
+ } finally {
55
+ fs.rmSync(temp, { recursive: true, force: true });
56
+ }
57
+ })();
@@ -1,63 +1,63 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { StorageLayout } = require('../lib/storage-layout');
6
- const { SettingsStore } = require('../lib/settings-store');
7
- const { ProjectRegistryStore } = require('../lib/project-registry-store');
8
- const { ProjectStore } = require('../lib/project-store');
9
-
10
- const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-kbpath-v2-'));
11
- const projectId = 'project-kbpath-follow';
12
- const oldKnowledgePath = path.join(dataDir, 'knowledge-old');
13
- const newKnowledgePath = path.join(dataDir, 'knowledge-new');
14
- process.env.KB_DATA_DIR = dataDir;
15
- require('../lib/data-dir')._resetCache();
16
-
17
- (async () => {
18
- try {
19
- fs.mkdirSync(oldKnowledgePath, { recursive: true });
20
- fs.mkdirSync(newKnowledgePath, { recursive: true });
21
- const layout = new StorageLayout({ dataDir });
22
- const settingsStore = new SettingsStore({ layout });
23
- const registryStore = new ProjectRegistryStore({ layout });
24
- const projectStore = new ProjectStore({ layout });
25
- await settingsStore.initialize({ knowledge: { rootPath: path.join(dataDir, 'future') } });
26
- await registryStore.initialize();
27
- await projectStore.create(projectId, { displayName: 'KB path follow', storageName: 'kbpath', repoPath: dataDir, knowledgePath: oldKnowledgePath });
28
- await registryStore.add(projectId, { displayNameSnapshot: 'KB path follow' });
29
-
30
- const runner = require('../lib/claude-cli-runner');
31
- const started = runner.startChatSession({
32
- slug: projectId,
33
- projectPath: dataDir,
34
- kbPath: oldKnowledgePath,
35
- aiProfile: { id: 'test', implementation: 'claude-code-agent', model: 'test-model' },
36
- });
37
- const recordPath = layout.getRuntimePath('claude-sessions', projectId, `${started.sessionId}.json`);
38
- const persisted = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
39
- persisted.claudeSessionId = 'stale-conversation-id';
40
- persisted.events.push({ type: 'claude/user-prompt', text: 'old conversation' });
41
- fs.writeFileSync(recordPath, `${JSON.stringify(persisted, null, 2)}\n`, 'utf8');
42
-
43
- await projectStore.updateConfig(projectId, { knowledgePath: newKnowledgePath }, { allowKnowledgePath: true });
44
- delete require.cache[require.resolve('../lib/claude-cli-runner')];
45
- const restoredRunner = require('../lib/claude-cli-runner');
46
- const events = [];
47
- restoredRunner.subscribe(started.sessionId, event => events.push(event));
48
- const restored = restoredRunner.getSession(started.sessionId);
49
- assert.strictEqual(restored.kbPath, newKnowledgePath);
50
- assert.strictEqual(restored.claudeSessionId, null);
51
- assert.strictEqual(restored.historyCleared, true);
52
- assert.strictEqual(restored.turns, 0);
53
- assert(events.some(event => event.type === 'claude/kbpath-updated' && event.fromKbPath === oldKnowledgePath && event.toKbPath === newKnowledgePath));
54
- assert(!restored.outputBuffer.some(event => event.type === 'claude/user-prompt'), 'stale conversation events must be cleared');
55
-
56
- const persistedAfter = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
57
- assert.strictEqual(persistedAfter.kbPath, newKnowledgePath);
58
- assert.strictEqual(persistedAfter.claudeSessionId, null);
59
- console.log('kbpath-follow-test PASS');
60
- } finally {
61
- fs.rmSync(dataDir, { recursive: true, force: true });
62
- }
63
- })().catch(error => { console.error(error); process.exitCode = 1; });
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { StorageLayout } = require('../lib/storage-layout');
6
+ const { SettingsStore } = require('../lib/settings-store');
7
+ const { ProjectRegistryStore } = require('../lib/project-registry-store');
8
+ const { ProjectStore } = require('../lib/project-store');
9
+
10
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkb-kbpath-v2-'));
11
+ const projectId = 'project-kbpath-follow';
12
+ const oldKnowledgePath = path.join(dataDir, 'knowledge-old');
13
+ const newKnowledgePath = path.join(dataDir, 'knowledge-new');
14
+ process.env.KB_DATA_DIR = dataDir;
15
+ require('../lib/data-dir')._resetCache();
16
+
17
+ (async () => {
18
+ try {
19
+ fs.mkdirSync(oldKnowledgePath, { recursive: true });
20
+ fs.mkdirSync(newKnowledgePath, { recursive: true });
21
+ const layout = new StorageLayout({ dataDir });
22
+ const settingsStore = new SettingsStore({ layout });
23
+ const registryStore = new ProjectRegistryStore({ layout });
24
+ const projectStore = new ProjectStore({ layout });
25
+ await settingsStore.initialize({ knowledge: { rootPath: path.join(dataDir, 'future') } });
26
+ await registryStore.initialize();
27
+ await projectStore.create(projectId, { displayName: 'KB path follow', storageName: 'kbpath', repoPath: dataDir, knowledgePath: oldKnowledgePath });
28
+ await registryStore.add(projectId, { displayNameSnapshot: 'KB path follow' });
29
+
30
+ const runner = require('../lib/claude-cli-runner');
31
+ const started = runner.startChatSession({
32
+ slug: projectId,
33
+ projectPath: dataDir,
34
+ kbPath: oldKnowledgePath,
35
+ aiProfile: { id: 'test', implementation: 'claude-code-agent', model: 'test-model' },
36
+ });
37
+ const recordPath = layout.getRuntimePath('claude-sessions', projectId, `${started.sessionId}.json`);
38
+ const persisted = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
39
+ persisted.claudeSessionId = 'stale-conversation-id';
40
+ persisted.events.push({ type: 'claude/user-prompt', text: 'old conversation' });
41
+ fs.writeFileSync(recordPath, `${JSON.stringify(persisted, null, 2)}\n`, 'utf8');
42
+
43
+ await projectStore.updateConfig(projectId, { knowledgePath: newKnowledgePath }, { allowKnowledgePath: true });
44
+ delete require.cache[require.resolve('../lib/claude-cli-runner')];
45
+ const restoredRunner = require('../lib/claude-cli-runner');
46
+ const events = [];
47
+ restoredRunner.subscribe(started.sessionId, event => events.push(event));
48
+ const restored = restoredRunner.getSession(started.sessionId);
49
+ assert.strictEqual(restored.kbPath, newKnowledgePath);
50
+ assert.strictEqual(restored.claudeSessionId, null);
51
+ assert.strictEqual(restored.historyCleared, true);
52
+ assert.strictEqual(restored.turns, 0);
53
+ assert(events.some(event => event.type === 'claude/kbpath-updated' && event.fromKbPath === oldKnowledgePath && event.toKbPath === newKnowledgePath));
54
+ assert(!restored.outputBuffer.some(event => event.type === 'claude/user-prompt'), 'stale conversation events must be cleared');
55
+
56
+ const persistedAfter = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
57
+ assert.strictEqual(persistedAfter.kbPath, newKnowledgePath);
58
+ assert.strictEqual(persistedAfter.claudeSessionId, null);
59
+ console.log('kbpath-follow-test PASS');
60
+ } finally {
61
+ fs.rmSync(dataDir, { recursive: true, force: true });
62
+ }
63
+ })().catch(error => { console.error(error); process.exitCode = 1; });
@@ -1,144 +1,144 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const { KnowledgeDatabase, sqlString, inPredicate } = require('../lib/knowledge-db');
6
- const { LocalEmbeddingService, QUERY_PREFIX } = require('../lib/embedding-service');
7
- const { EMBEDDING_DIMENSIONS, sha256 } = require('../lib/knowledge-schema');
8
- const { MarkdownKnowledgeIndexer, chunkMarkdown } = require('../lib/markdown-knowledge-indexer');
9
-
10
- function vectorAt(index) {
11
- const vector = new Array(EMBEDDING_DIMENSIONS).fill(0);
12
- vector[index] = 1;
13
- return vector;
14
- }
15
-
16
- (async () => {
17
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-db-test-'));
18
- const db = new KnowledgeDatabase({ dbPath: path.join(temp, 'knowledge.lancedb') });
19
- try {
20
- assert.equal(sqlString("space'o"), "'space''o'");
21
- assert.equal(inPredicate('space_id', ['a', 'a', 'b']), "space_id IN ('a', 'b')");
22
- assert.throws(() => inPredicate('space_id', []), /must not be empty/);
23
-
24
- const created = await db.replaceEntry('personal:alpha', 'modules/auth.md', [
25
- {
26
- chunkOrder: 0,
27
- title: '登录设计',
28
- chunkText: '系统使用短期访问令牌和可轮换刷新令牌。',
29
- vector: vectorAt(0),
30
- tags: ['auth'],
31
- sourcePaths: ['src/auth.js'],
32
- sourceCommit: 'abc123',
33
- },
34
- {
35
- chunkOrder: 1,
36
- title: '登录设计',
37
- chunkText: '退出登录会撤销刷新令牌。',
38
- vector: vectorAt(1),
39
- sourceCommit: 'abc123',
40
- },
41
- ]);
42
- assert.equal(created.action, 'created');
43
- assert.equal(await db.count(), 2);
44
-
45
- const same = await db.replaceEntry('personal:alpha', 'modules/auth.md', [
46
- { chunkOrder: 0, title: '登录设计', chunkText: '系统使用短期访问令牌和可轮换刷新令牌。', vector: vectorAt(0), tags: ['auth'], sourcePaths: ['src/auth.js'], sourceCommit: 'abc123' },
47
- { chunkOrder: 1, title: '登录设计', chunkText: '退出登录会撤销刷新令牌。', vector: vectorAt(1), sourceCommit: 'abc123' },
48
- ]);
49
- assert.equal(same.action, 'unchanged');
50
-
51
- const updated = await db.replaceEntry('personal:alpha', 'modules/auth.md', [
52
- { chunkOrder: 0, title: '登录设计', chunkText: '系统使用短期访问令牌。', vector: vectorAt(0), sourceCommit: 'def456' },
53
- ]);
54
- assert.equal(updated.action, 'updated');
55
- assert.equal(updated.deleted, 1);
56
- assert.equal(await db.count(), 1);
57
-
58
- await db.replaceEntry('personal:beta', 'GOAL.md', [
59
- { chunkOrder: 0, title: '目标', chunkText: '支付服务负责对账。', vector: vectorAt(1), sourceCommit: '999999' },
60
- ]);
61
- assert.equal(await db.count(['personal:alpha']), 1);
62
- const scoped = await db.vectorSearch(vectorAt(0), { spaceIds: ['personal:alpha'], limit: 5 });
63
- assert.equal(scoped.length, 1);
64
- assert.equal(scoped[0].space_id, 'personal:alpha');
65
- assert.deepEqual(scoped[0].tags, []);
66
- assert.equal(scoped[0].content_hash, sha256('系统使用短期访问令牌。'));
67
-
68
- const initialIndexes = await db.ensureSearchIndexes();
69
- assert.equal(initialIndexes.ftsSchemaVersion, 2);
70
- assert.equal(initialIndexes.ftsUpgradeRequired, false);
71
- assert.equal(db.maintenanceState().ftsSchemaVersion, 2);
72
- const keyword = await db.fullTextSearch('短期访问令牌', { spaceIds: ['personal:alpha'], limit: 5 });
73
- assert.equal(keyword.length, 1);
74
- const hybrid = await db.hybridSearch({ text: '短期访问令牌', vector: vectorAt(0), spaceIds: ['personal:alpha'], limit: 5 });
75
- assert.equal(hybrid.length, 1);
76
- assert.deepEqual(hybrid[0].match_channels.sort(), ['keyword', 'semantic']);
77
-
78
- assert.equal((await db.deleteEntry('personal:alpha', 'modules/auth.md')).deleted, 1);
79
- assert.equal((await db.deleteSpace('personal:beta')).deleted, 1);
80
- assert.equal(await db.count(), 0);
81
-
82
- let seenInput = null;
83
- let seenOptions = null;
84
- const embeddings = new LocalEmbeddingService({
85
- remoteHost: 'https://models.example.test/',
86
- pipelineFactory: async (task, model, options) => {
87
- assert.equal(task, 'feature-extraction');
88
- assert.equal(model, 'Xenova/bge-small-zh-v1.5');
89
- assert.equal(options.dtype, 'fp32');
90
- return async (input, embedOptions) => {
91
- seenInput = input;
92
- seenOptions = embedOptions;
93
- return { data: Float32Array.from(vectorAt(2)) };
94
- };
95
- },
96
- });
97
- const embedded = await embeddings.embedQuery('登录怎么做');
98
- assert.equal(embedded.length, EMBEDDING_DIMENSIONS);
99
- assert.equal(seenInput, `${QUERY_PREFIX}登录怎么做`);
100
- assert.deepEqual(seenOptions, { pooling: 'cls', normalize: true });
101
- assert.equal(embeddings.status().loaded, true);
102
- assert.equal(embeddings.status().remoteHost, 'https://models.example.test/');
103
-
104
- const sections = chunkMarkdown('# 标题\n\n第一段。\n\n## 细节\n\n第二段。', { maxChars: 20, overlapChars: 2 });
105
- assert.ok(sections.length >= 2);
106
- assert.deepEqual(sections.at(-1).headingPath, ['标题', '细节']);
107
-
108
- const markdownRoot = path.join(temp, 'legacy-markdown');
109
- fs.mkdirSync(path.join(markdownRoot, 'modules'), { recursive: true });
110
- fs.writeFileSync(path.join(markdownRoot, 'GOAL.md'), '# 项目目标\n\n建立可靠的支付知识库。\n', 'utf8');
111
- fs.writeFileSync(path.join(markdownRoot, 'modules', 'pay.md'), '# 支付模块\n\n对账任务每天运行。\n', 'utf8');
112
- fs.writeFileSync(path.join(markdownRoot, 'modules', '00-index.md'), '# Modules Index\n\nTags: duplicated, derived\n', 'utf8');
113
- const deterministicEmbedder = {
114
- embedPassage: async text => text.includes('对账') ? vectorAt(3) : vectorAt(4),
115
- };
116
- const indexer = new MarkdownKnowledgeIndexer({ database: db, embedder: deterministicEmbedder });
117
- await db.replaceEntry('personal:migrated', 'modules/00-index.md', [
118
- { chunkOrder: 0, title: 'Legacy index', chunkText: 'duplicated derived tags', vector: vectorAt(5) },
119
- ]);
120
- const firstIndex = await indexer.indexDirectory({ kbPath: markdownRoot, spaceId: 'personal:migrated', sourceProjectId: 'pay' });
121
- assert.equal(firstIndex.files, 2);
122
- assert.equal(firstIndex.indexed, 2);
123
- assert.equal(firstIndex.deletedEntries, 1, 'previously indexed derived files should be removed as stale');
124
- assert(!firstIndex.results.some(item => item.entryId.endsWith('00-index.md')), 'derived indexes must not be embedded');
125
- assert.equal(firstIndex.maintenance.optimized, false, 'small updates must not optimize on every commit');
126
- const secondIndex = await indexer.indexDirectory({ kbPath: markdownRoot, spaceId: 'personal:migrated', sourceProjectId: 'pay' });
127
- assert.equal(secondIndex.unchanged, 2);
128
- db.optimizeOperationsThreshold = 1;
129
- fs.unlinkSync(path.join(markdownRoot, 'GOAL.md'));
130
- const thirdIndex = await indexer.indexDirectory({ kbPath: markdownRoot, spaceId: 'personal:migrated', sourceProjectId: 'pay' });
131
- assert.equal(thirdIndex.deletedEntries, 1);
132
- assert.equal(thirdIndex.maintenance.optimized, true, 'maintenance should run after the configured mutation threshold');
133
- assert.equal(db.maintenanceState().pendingOperations, 0);
134
- assert.deepEqual(await db.entryIds('personal:migrated'), ['modules/pay.md']);
135
-
136
- console.log('knowledge-db-test: PASS');
137
- } finally {
138
- await db.close();
139
- fs.rmSync(temp, { recursive: true, force: true });
140
- }
141
- })().catch(error => {
142
- console.error(error);
143
- process.exit(1);
144
- });
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { KnowledgeDatabase, sqlString, inPredicate } = require('../lib/knowledge-db');
6
+ const { LocalEmbeddingService, QUERY_PREFIX } = require('../lib/embedding-service');
7
+ const { EMBEDDING_DIMENSIONS, sha256 } = require('../lib/knowledge-schema');
8
+ const { MarkdownKnowledgeIndexer, chunkMarkdown } = require('../lib/markdown-knowledge-indexer');
9
+
10
+ function vectorAt(index) {
11
+ const vector = new Array(EMBEDDING_DIMENSIONS).fill(0);
12
+ vector[index] = 1;
13
+ return vector;
14
+ }
15
+
16
+ (async () => {
17
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-db-test-'));
18
+ const db = new KnowledgeDatabase({ dbPath: path.join(temp, 'knowledge.lancedb') });
19
+ try {
20
+ assert.equal(sqlString("space'o"), "'space''o'");
21
+ assert.equal(inPredicate('space_id', ['a', 'a', 'b']), "space_id IN ('a', 'b')");
22
+ assert.throws(() => inPredicate('space_id', []), /must not be empty/);
23
+
24
+ const created = await db.replaceEntry('personal:alpha', 'modules/auth.md', [
25
+ {
26
+ chunkOrder: 0,
27
+ title: '登录设计',
28
+ chunkText: '系统使用短期访问令牌和可轮换刷新令牌。',
29
+ vector: vectorAt(0),
30
+ tags: ['auth'],
31
+ sourcePaths: ['src/auth.js'],
32
+ sourceCommit: 'abc123',
33
+ },
34
+ {
35
+ chunkOrder: 1,
36
+ title: '登录设计',
37
+ chunkText: '退出登录会撤销刷新令牌。',
38
+ vector: vectorAt(1),
39
+ sourceCommit: 'abc123',
40
+ },
41
+ ]);
42
+ assert.equal(created.action, 'created');
43
+ assert.equal(await db.count(), 2);
44
+
45
+ const same = await db.replaceEntry('personal:alpha', 'modules/auth.md', [
46
+ { chunkOrder: 0, title: '登录设计', chunkText: '系统使用短期访问令牌和可轮换刷新令牌。', vector: vectorAt(0), tags: ['auth'], sourcePaths: ['src/auth.js'], sourceCommit: 'abc123' },
47
+ { chunkOrder: 1, title: '登录设计', chunkText: '退出登录会撤销刷新令牌。', vector: vectorAt(1), sourceCommit: 'abc123' },
48
+ ]);
49
+ assert.equal(same.action, 'unchanged');
50
+
51
+ const updated = await db.replaceEntry('personal:alpha', 'modules/auth.md', [
52
+ { chunkOrder: 0, title: '登录设计', chunkText: '系统使用短期访问令牌。', vector: vectorAt(0), sourceCommit: 'def456' },
53
+ ]);
54
+ assert.equal(updated.action, 'updated');
55
+ assert.equal(updated.deleted, 1);
56
+ assert.equal(await db.count(), 1);
57
+
58
+ await db.replaceEntry('personal:beta', 'GOAL.md', [
59
+ { chunkOrder: 0, title: '目标', chunkText: '支付服务负责对账。', vector: vectorAt(1), sourceCommit: '999999' },
60
+ ]);
61
+ assert.equal(await db.count(['personal:alpha']), 1);
62
+ const scoped = await db.vectorSearch(vectorAt(0), { spaceIds: ['personal:alpha'], limit: 5 });
63
+ assert.equal(scoped.length, 1);
64
+ assert.equal(scoped[0].space_id, 'personal:alpha');
65
+ assert.deepEqual(scoped[0].tags, []);
66
+ assert.equal(scoped[0].content_hash, sha256('系统使用短期访问令牌。'));
67
+
68
+ const initialIndexes = await db.ensureSearchIndexes();
69
+ assert.equal(initialIndexes.ftsSchemaVersion, 2);
70
+ assert.equal(initialIndexes.ftsUpgradeRequired, false);
71
+ assert.equal(db.maintenanceState().ftsSchemaVersion, 2);
72
+ const keyword = await db.fullTextSearch('短期访问令牌', { spaceIds: ['personal:alpha'], limit: 5 });
73
+ assert.equal(keyword.length, 1);
74
+ const hybrid = await db.hybridSearch({ text: '短期访问令牌', vector: vectorAt(0), spaceIds: ['personal:alpha'], limit: 5 });
75
+ assert.equal(hybrid.length, 1);
76
+ assert.deepEqual(hybrid[0].match_channels.sort(), ['keyword', 'semantic']);
77
+
78
+ assert.equal((await db.deleteEntry('personal:alpha', 'modules/auth.md')).deleted, 1);
79
+ assert.equal((await db.deleteSpace('personal:beta')).deleted, 1);
80
+ assert.equal(await db.count(), 0);
81
+
82
+ let seenInput = null;
83
+ let seenOptions = null;
84
+ const embeddings = new LocalEmbeddingService({
85
+ remoteHost: 'https://models.example.test/',
86
+ pipelineFactory: async (task, model, options) => {
87
+ assert.equal(task, 'feature-extraction');
88
+ assert.equal(model, 'Xenova/bge-small-zh-v1.5');
89
+ assert.equal(options.dtype, 'fp32');
90
+ return async (input, embedOptions) => {
91
+ seenInput = input;
92
+ seenOptions = embedOptions;
93
+ return { data: Float32Array.from(vectorAt(2)) };
94
+ };
95
+ },
96
+ });
97
+ const embedded = await embeddings.embedQuery('登录怎么做');
98
+ assert.equal(embedded.length, EMBEDDING_DIMENSIONS);
99
+ assert.equal(seenInput, `${QUERY_PREFIX}登录怎么做`);
100
+ assert.deepEqual(seenOptions, { pooling: 'cls', normalize: true });
101
+ assert.equal(embeddings.status().loaded, true);
102
+ assert.equal(embeddings.status().remoteHost, 'https://models.example.test/');
103
+
104
+ const sections = chunkMarkdown('# 标题\n\n第一段。\n\n## 细节\n\n第二段。', { maxChars: 20, overlapChars: 2 });
105
+ assert.ok(sections.length >= 2);
106
+ assert.deepEqual(sections.at(-1).headingPath, ['标题', '细节']);
107
+
108
+ const markdownRoot = path.join(temp, 'legacy-markdown');
109
+ fs.mkdirSync(path.join(markdownRoot, 'modules'), { recursive: true });
110
+ fs.writeFileSync(path.join(markdownRoot, 'GOAL.md'), '# 项目目标\n\n建立可靠的支付知识库。\n', 'utf8');
111
+ fs.writeFileSync(path.join(markdownRoot, 'modules', 'pay.md'), '# 支付模块\n\n对账任务每天运行。\n', 'utf8');
112
+ fs.writeFileSync(path.join(markdownRoot, 'modules', '00-index.md'), '# Modules Index\n\nTags: duplicated, derived\n', 'utf8');
113
+ const deterministicEmbedder = {
114
+ embedPassage: async text => text.includes('对账') ? vectorAt(3) : vectorAt(4),
115
+ };
116
+ const indexer = new MarkdownKnowledgeIndexer({ database: db, embedder: deterministicEmbedder });
117
+ await db.replaceEntry('personal:migrated', 'modules/00-index.md', [
118
+ { chunkOrder: 0, title: 'Legacy index', chunkText: 'duplicated derived tags', vector: vectorAt(5) },
119
+ ]);
120
+ const firstIndex = await indexer.indexDirectory({ kbPath: markdownRoot, spaceId: 'personal:migrated', sourceProjectId: 'pay' });
121
+ assert.equal(firstIndex.files, 2);
122
+ assert.equal(firstIndex.indexed, 2);
123
+ assert.equal(firstIndex.deletedEntries, 1, 'previously indexed derived files should be removed as stale');
124
+ assert(!firstIndex.results.some(item => item.entryId.endsWith('00-index.md')), 'derived indexes must not be embedded');
125
+ assert.equal(firstIndex.maintenance.optimized, false, 'small updates must not optimize on every commit');
126
+ const secondIndex = await indexer.indexDirectory({ kbPath: markdownRoot, spaceId: 'personal:migrated', sourceProjectId: 'pay' });
127
+ assert.equal(secondIndex.unchanged, 2);
128
+ db.optimizeOperationsThreshold = 1;
129
+ fs.unlinkSync(path.join(markdownRoot, 'GOAL.md'));
130
+ const thirdIndex = await indexer.indexDirectory({ kbPath: markdownRoot, spaceId: 'personal:migrated', sourceProjectId: 'pay' });
131
+ assert.equal(thirdIndex.deletedEntries, 1);
132
+ assert.equal(thirdIndex.maintenance.optimized, true, 'maintenance should run after the configured mutation threshold');
133
+ assert.equal(db.maintenanceState().pendingOperations, 0);
134
+ assert.deepEqual(await db.entryIds('personal:migrated'), ['modules/pay.md']);
135
+
136
+ console.log('knowledge-db-test: PASS');
137
+ } finally {
138
+ await db.close();
139
+ fs.rmSync(temp, { recursive: true, force: true });
140
+ }
141
+ })().catch(error => {
142
+ console.error(error);
143
+ process.exit(1);
144
+ });