@sanqianx/project-knowledge 4.6.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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/CHANGELOG.md +780 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +266 -0
- package/_site/README.md +30 -0
- package/_site/_test/ai-profile-resolver-test.js +137 -0
- package/_site/_test/ai-profile-test.js +75 -0
- package/_site/_test/ai-vendor-presets-test.js +91 -0
- package/_site/_test/automation-queue-test.js +14 -0
- package/_site/_test/automation-ui-test.js +40 -0
- package/_site/_test/background-task-registry-test.js +43 -0
- package/_site/_test/baseline-schema-test.js +91 -0
- package/_site/_test/bin-cli-test.js +218 -0
- package/_site/_test/bridge-adapter-test.js +95 -0
- package/_site/_test/bridge-consumer-service-test.js +310 -0
- package/_site/_test/cdp-browser-failure-test.js +100 -0
- package/_site/_test/chat-claudecodeui-match-test.js +92 -0
- package/_site/_test/claude-executable-discovery-test.js +90 -0
- package/_site/_test/claude-session-lifecycle-sweep-test.js +252 -0
- package/_site/_test/claude-workbench-test.js +170 -0
- package/_site/_test/client-capture-restart-e2e-test.js +189 -0
- package/_site/_test/codex-conversation-projection-test.js +59 -0
- package/_site/_test/codex-notify-fanout-test.js +45 -0
- package/_site/_test/commit-boundary-freeze-test.js +147 -0
- package/_site/_test/commit-conversation-binding-test.js +99 -0
- package/_site/_test/commit-evidence-test.js +110 -0
- package/_site/_test/context-pack-test.js +198 -0
- package/_site/_test/conversation-api-test.js +119 -0
- package/_site/_test/conversation-store-test.js +136 -0
- package/_site/_test/cross-repo-isolation-e2e-test.js +187 -0
- package/_site/_test/data-dir-migration-test.js +77 -0
- package/_site/_test/desktop-browser-compat-test.js +28 -0
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -0
- package/_site/_test/embedding-config-test.js +32 -0
- package/_site/_test/explicit-commit-processor-test.js +37 -0
- package/_site/_test/fixtures/fake-browser-exit.js +10 -0
- package/_site/_test/fixtures/fake-browser-hang.js +10 -0
- package/_site/_test/fixtures/make-git-repos.js +89 -0
- package/_site/_test/folder-picker-output-test.js +19 -0
- package/_site/_test/full-integration-e2e-test.js +165 -0
- package/_site/_test/git-validation-test.js +81 -0
- package/_site/_test/hook-runtime-endpoint-test.js +88 -0
- package/_site/_test/hook-status-repair-api-test.js +152 -0
- package/_site/_test/hook-trigger-test.js +85 -0
- package/_site/_test/hook-worktree-test.js +42 -0
- package/_site/_test/import-preflight-api-test.js +233 -0
- package/_site/_test/import-ui-flow-test.js +171 -0
- package/_site/_test/index-builder-test.js +41 -0
- package/_site/_test/index-writer-concurrency-test.js +135 -0
- package/_site/_test/integration-adapters-test.js +243 -0
- package/_site/_test/integration-capture-setup-test.js +108 -0
- package/_site/_test/integration-surface-coverage-test.js +165 -0
- package/_site/_test/kb-framework-test.js +57 -0
- package/_site/_test/kbpath-follow-test.js +63 -0
- package/_site/_test/knowledge-db-test.js +144 -0
- package/_site/_test/knowledge-language-control-test.js +170 -0
- package/_site/_test/knowledge-maintenance-test.js +40 -0
- package/_site/_test/knowledge-migration-test.js +75 -0
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -0
- package/_site/_test/knowledge-query-test.js +54 -0
- package/_site/_test/knowledge-retrieval-service-test.js +77 -0
- package/_site/_test/knowledge-scopes-test.js +37 -0
- package/_site/_test/knowledge-storage-location-test.js +28 -0
- package/_site/_test/knowledge-storage-startup-test.js +52 -0
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -0
- package/_site/_test/legacy-claude-session-alias-test.js +106 -0
- package/_site/_test/legacy-forward-compat-test.js +257 -0
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -0
- package/_site/_test/legacy-routes-removed-test.js +47 -0
- package/_site/_test/legacy-workbench-exclusion-test.js +92 -0
- package/_site/_test/log-redaction-test.js +58 -0
- package/_site/_test/logging-api-test.js +89 -0
- package/_site/_test/logging-sse-no-gap-test.js +88 -0
- package/_site/_test/logging-ui-test.js +104 -0
- package/_site/_test/markdown-delta-overlay-test.js +85 -0
- package/_site/_test/markdown-maintenance-api-test.js +75 -0
- package/_site/_test/markdown-maintenance-test.js +11 -0
- package/_site/_test/mcp-server-test.js +149 -0
- package/_site/_test/model-context-windows-test.js +89 -0
- package/_site/_test/module-bridge-eventbridge-test.js +193 -0
- package/_site/_test/non-release-ci-test.js +34 -0
- package/_site/_test/offline-boundary-isolation-test.js +32 -0
- package/_site/_test/orphaned-claim-recovery-test.js +80 -0
- package/_site/_test/p0-data-migration-characterization-test.js +37 -0
- package/_site/_test/p0-e2e-gate-test.js +347 -0
- package/_site/_test/package-boundary-test.js +42 -0
- package/_site/_test/package-startup-test.js +56 -0
- package/_site/_test/packaged-ui-smoke-test.js +79 -0
- package/_site/_test/path-consistency-test.js +151 -0
- package/_site/_test/pending-sweep-test.js +9 -0
- package/_site/_test/post-commit-automation-test.js +87 -0
- package/_site/_test/pr-consumer-contract-test.js +55 -0
- package/_site/_test/project-control-panel-task14-test.js +63 -0
- package/_site/_test/project-delete-recovery-test.js +64 -0
- package/_site/_test/project-goal-editor-test.js +145 -0
- package/_site/_test/project-layout-v2-migration-test.js +127 -0
- package/_site/_test/project-lifecycle-transaction-test.js +106 -0
- package/_site/_test/project-remove-running-guard-test.js +79 -0
- package/_site/_test/project-store-test.js +90 -0
- package/_site/_test/prompt-settings-test.js +115 -0
- package/_site/_test/protected-architecture-gate-test.js +123 -0
- package/_site/_test/refactor-characterization-test.js +36 -0
- package/_site/_test/release-version-sync-test.js +98 -0
- package/_site/_test/requirement-binding-test.js +113 -0
- package/_site/_test/requirement-recorder-test.js +173 -0
- package/_site/_test/run-all-tests.js +156 -0
- package/_site/_test/runtime-endpoint-test.js +64 -0
- package/_site/_test/scanner-test.js +83 -0
- package/_site/_test/server-runtime-migration-safety-test.js +27 -0
- package/_site/_test/server-security-test.js +96 -0
- package/_site/_test/sessions-stream-test.js +92 -0
- package/_site/_test/shared-contracts-test.js +58 -0
- package/_site/_test/simple-import-test.js +97 -0
- package/_site/_test/startup-analysis-disabled-test.js +16 -0
- package/_site/_test/storage-foundation-test.js +78 -0
- package/_site/_test/structured-logger-test.js +101 -0
- package/_site/_test/task15-20-ui-flow-test.js +148 -0
- package/_site/_test/team-knowledge-store-sparse-checkout-test.js +196 -0
- package/_site/_test/team-knowledge-store-test.js +131 -0
- package/_site/_test/tracking-start-test.js +143 -0
- package/_site/_test/ui-i18n-toggle-test.js +114 -0
- package/_site/_test/ui-smoke-test.js +73 -0
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -0
- package/_site/_test/workbench-permission-test.js +118 -0
- package/_site/_test/workspace-ui-contract-test.js +43 -0
- package/_site/lib/ai-profile-resolver.js +78 -0
- package/_site/lib/ai-vendor-presets.js +83 -0
- package/_site/lib/ai-workspace.js +104 -0
- package/_site/lib/atomic-file.js +204 -0
- package/_site/lib/automation-config.js +140 -0
- package/_site/lib/bridge-adapter.js +256 -0
- package/_site/lib/bridge-consumer-service.js +402 -0
- package/_site/lib/claude-cli-runner.js +1599 -0
- package/_site/lib/commit-conversation-binder.js +246 -0
- package/_site/lib/commit-processing-ledger.js +27 -0
- package/_site/lib/commit-prompt.js +202 -0
- package/_site/lib/commit-reconciler.js +444 -0
- package/_site/lib/context-pack-builder.js +299 -0
- package/_site/lib/contracts.js +279 -0
- package/_site/lib/conversation-exclusions.js +204 -0
- package/_site/lib/conversation-query-service.js +161 -0
- package/_site/lib/conversation-store.js +385 -0
- package/_site/lib/data-dir.js +81 -0
- package/_site/lib/data-state-classifier.js +40 -0
- package/_site/lib/embedding-service.js +85 -0
- package/_site/lib/evidence-bundle.js +228 -0
- package/_site/lib/folder-picker-output.js +24 -0
- package/_site/lib/git-runner.js +62 -0
- package/_site/lib/github-team-store.js +1175 -0
- package/_site/lib/hook-manager.js +202 -0
- package/_site/lib/index-builder.js +157 -0
- package/_site/lib/index-service.js +173 -0
- package/_site/lib/integration-installer.js +907 -0
- package/_site/lib/kb-framework.js +191 -0
- package/_site/lib/kb-validator.js +124 -0
- package/_site/lib/knowledge-db.js +363 -0
- package/_site/lib/knowledge-promotion.js +435 -0
- package/_site/lib/knowledge-retrieval-service.js +401 -0
- package/_site/lib/knowledge-schema.js +121 -0
- package/_site/lib/knowledge-tool-runtime.js +359 -0
- package/_site/lib/legacy-data-manifest.js +26 -0
- package/_site/lib/llm-client.js +173 -0
- package/_site/lib/markdown-knowledge-indexer.js +230 -0
- package/_site/lib/migration-service.js +442 -0
- package/_site/lib/model-context-windows.js +82 -0
- package/_site/lib/module-bridge.js +496 -0
- package/_site/lib/post-commit-automation.js +141 -0
- package/_site/lib/project-lifecycle-service.js +511 -0
- package/_site/lib/project-registry-store.js +129 -0
- package/_site/lib/project-store.js +262 -0
- package/_site/lib/prompt-registry.js +148 -0
- package/_site/lib/requirement-adapters.js +10 -0
- package/_site/lib/requirement-binder.js +112 -0
- package/_site/lib/requirement-recorder.js +278 -0
- package/_site/lib/runtime-endpoint.js +160 -0
- package/_site/lib/scanner.js +298 -0
- package/_site/lib/server-app.js +1520 -0
- package/_site/lib/settings-store.js +113 -0
- package/_site/lib/storage-layout.js +162 -0
- package/_site/lib/structured-logger.js +574 -0
- package/_site/scripts/codex-notify-fanout.cjs +51 -0
- package/_site/scripts/folder-picker.ps1 +156 -0
- package/_site/scripts/hook-trigger.js +156 -0
- package/_site/scripts/sync-release-version.js +158 -0
- package/_site/server.js +12 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/bin/project-knowledge-integrations.js +241 -0
- package/bin/project-knowledge-kb.js +65 -0
- package/bin/project-knowledge-mcp.js +195 -0
- package/bin/project-knowledge.js +439 -0
- package/claude-prompts.json +21 -0
- package/docs/README.zh-CN.md +175 -0
- package/docs/assets/architecture.svg +182 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/draft-review.png +0 -0
- package/docs/assets/logo.svg +50 -0
- package/docs/assets/runs-drafts.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/project-registry-schema.md +33 -0
- package/docs/team-knowledge-mode-a-plan.md +73 -0
- package/docs/testing-strategy.md +33 -0
- package/package.json +100 -0
- package/plugins/project-knowledge/.claude-plugin/plugin.json +19 -0
- package/plugins/project-knowledge/.codex-plugin/plugin.json +38 -0
- package/plugins/project-knowledge/.mcp.json +14 -0
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -0
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -0
- package/plugins/project-knowledge/skills/project-knowledge/agents/openai.yaml +11 -0
- package/templates/change.md +30 -0
- package/templates/project-readme.md +52 -0
- package/ui/app.css +58 -0
- package/ui/app.js +607 -0
- package/ui/favicon.svg +38 -0
- package/ui/i18n.js +146 -0
- package/ui/index.html +162 -0
|
@@ -0,0 +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
|
+
})();
|
|
@@ -0,0 +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; });
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// _site/_test/knowledge-language-control-test.js
|
|
2
|
+
//
|
|
3
|
+
// T11: knowledge output language controls. Verifies:
|
|
4
|
+
// - import accepts knowledgeLanguage ('zh-CN' / 'en-US');
|
|
5
|
+
// - per-project PATCH updates knowledgeLanguage;
|
|
6
|
+
// - the value is persisted in config.knowledgeLanguage;
|
|
7
|
+
// - the value is what the next analyzer invocation would see (we
|
|
8
|
+
// verify it via the public state API which surfaces the same
|
|
9
|
+
// config object the reconciler reads);
|
|
10
|
+
// - changing language does NOT silently rewrite historical knowledge;
|
|
11
|
+
// - invalid values are rejected with 400.
|
|
12
|
+
|
|
13
|
+
const assert = require('assert');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
const { spawnServer } = require('./helpers/spawn-server');
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
21
|
+
const sitePort = 7990 + (process.pid % 200);
|
|
22
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-knowledge-language-' + process.pid + '-'));
|
|
23
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-knowledge-language-repo-' + process.pid + '-'));
|
|
24
|
+
|
|
25
|
+
function git(args) {
|
|
26
|
+
const result = spawnSync('git', ['-C', repo, ...args], {
|
|
27
|
+
encoding: 'utf8', windowsHide: true,
|
|
28
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
29
|
+
});
|
|
30
|
+
if (result.status !== 0) throw new Error(result.stderr || `git ${args.join(' ')} failed: ${result.status}`);
|
|
31
|
+
return String(result.stdout || '').trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function serverFetch(url, init, timeoutMs = 10000) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
39
|
+
const text = await response.text();
|
|
40
|
+
return { status: response.status, body: text ? JSON.parse(text) : {} };
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
(async () => {
|
|
47
|
+
git(['init', '-q', '-b', 'main']);
|
|
48
|
+
git(['config', 'user.email', 'kl@example.local']);
|
|
49
|
+
git(['config', 'user.name', 'Knowledge Language']);
|
|
50
|
+
fs.writeFileSync(path.join(repo, 'README.md'), '# kl\n');
|
|
51
|
+
git(['add', 'README.md']);
|
|
52
|
+
git(['commit', '-q', '-m', 'baseline']);
|
|
53
|
+
const knowledgeRoot = path.join(dataDir, 'knowledge');
|
|
54
|
+
fs.mkdirSync(knowledgeRoot, { recursive: true });
|
|
55
|
+
const server = spawnServer({
|
|
56
|
+
root: ROOT, port: sitePort, dataDir, tag: 'knowledge-language',
|
|
57
|
+
extraEnv: { KB_AUTOMATION_FAKE_CLAUDE: '1', KB_EMBEDDING_FAKE: '1', KB_SKIP_MIGRATION: '1' },
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
// waitForServer-equivalent
|
|
61
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
62
|
+
try {
|
|
63
|
+
const r = await serverFetch(`http://127.0.0.1:${sitePort}/api/health`);
|
|
64
|
+
if (r.body.ok) break;
|
|
65
|
+
} catch {}
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
67
|
+
}
|
|
68
|
+
await serverFetch(`http://127.0.0.1:${sitePort}/api/settings`, {
|
|
69
|
+
method: 'PATCH', headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ knowledge: { rootPath: knowledgeRoot } }),
|
|
71
|
+
});
|
|
72
|
+
await serverFetch(`http://127.0.0.1:${sitePort}/api/ai-profiles`, {
|
|
73
|
+
method: 'PUT', headers: { 'content-type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
schema: 'ai-profiles/v1', defaultProfileId: 'fake',
|
|
76
|
+
profiles: [{ id: 'fake', name: 'Fake', enabled: true, vendor: 'anthropic', model: 'fake', apiKeyUpdate: { mode: 'replace', value: 'k' } }],
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Case 1: import with zh-CN (default)
|
|
81
|
+
let r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/import`, {
|
|
82
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ localPath: repo, aiProfileId: 'fake', knowledgeLanguage: 'zh-CN' }),
|
|
84
|
+
});
|
|
85
|
+
assert.strictEqual(r.status, 201, JSON.stringify(r.body));
|
|
86
|
+
assert.strictEqual(r.body.config.knowledgeLanguage, 'zh-CN');
|
|
87
|
+
const PROJECT_ID = r.body.projectId;
|
|
88
|
+
|
|
89
|
+
// Case 2: import with en-US
|
|
90
|
+
const repo2 = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-kl-repo2-' + process.pid + '-'));
|
|
91
|
+
const git2 = (args) => git(['-C', repo2, ...args]);
|
|
92
|
+
git2(['init', '-q', '-b', 'main']);
|
|
93
|
+
git2(['config', 'user.email', 'kl2@example.local']);
|
|
94
|
+
git2(['config', 'user.name', 'KL2']);
|
|
95
|
+
fs.writeFileSync(path.join(repo2, 'README.md'), '# en\n');
|
|
96
|
+
git2(['add', 'README.md']);
|
|
97
|
+
git2(['commit', '-q', '-m', 'baseline']);
|
|
98
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/import`, {
|
|
99
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ localPath: repo2, aiProfileId: 'fake', knowledgeLanguage: 'en-US' }),
|
|
101
|
+
});
|
|
102
|
+
assert.strictEqual(r.status, 201);
|
|
103
|
+
assert.strictEqual(r.body.config.knowledgeLanguage, 'en-US');
|
|
104
|
+
fs.rmSync(repo2, { recursive: true, force: true });
|
|
105
|
+
|
|
106
|
+
// Case 3: invalid language rejected
|
|
107
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/import`, {
|
|
108
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ localPath: repo2, aiProfileId: 'fake', knowledgeLanguage: 'xx-INVALID' }),
|
|
110
|
+
});
|
|
111
|
+
assert.strictEqual(r.status, 400);
|
|
112
|
+
assert.strictEqual(r.body.error.code, 'INVALID_ARGUMENT');
|
|
113
|
+
|
|
114
|
+
// Case 4: per-project PATCH updates knowledgeLanguage
|
|
115
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${PROJECT_ID}`, {
|
|
116
|
+
method: 'PATCH', headers: { 'content-type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ knowledgeLanguage: 'en-US' }),
|
|
118
|
+
});
|
|
119
|
+
assert.strictEqual(r.status, 200, JSON.stringify(r.body));
|
|
120
|
+
assert.strictEqual(r.body.config.knowledgeLanguage, 'en-US');
|
|
121
|
+
// Verify state-side persists
|
|
122
|
+
const readBack = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${PROJECT_ID}`, undefined);
|
|
123
|
+
assert.strictEqual(readBack.body.project.config.knowledgeLanguage, 'en-US');
|
|
124
|
+
|
|
125
|
+
// Case 5: invalid value via PATCH also rejected
|
|
126
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${PROJECT_ID}`, {
|
|
127
|
+
method: 'PATCH', headers: { 'content-type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ knowledgeLanguage: 'klingon' }),
|
|
129
|
+
});
|
|
130
|
+
assert.strictEqual(r.status, 400);
|
|
131
|
+
|
|
132
|
+
// Case 6: project status surfaces knowledgeLanguage in the public state.
|
|
133
|
+
// The CommitReconciler reads config.knowledgeLanguage via projectStore,
|
|
134
|
+
// so anything the UI sees the analyzer also sees.
|
|
135
|
+
const stateR = await serverFetch(`http://127.0.0.1:${sitePort}/api/state`, undefined);
|
|
136
|
+
assert.strictEqual(stateR.status, 200);
|
|
137
|
+
const project = stateR.body.projects.find(p => p.projectId === PROJECT_ID);
|
|
138
|
+
assert(project, 'project must appear in /api/state');
|
|
139
|
+
assert.strictEqual(project.config.knowledgeLanguage, 'en-US');
|
|
140
|
+
|
|
141
|
+
// Case 7: changing language does NOT silently rewrite historical files.
|
|
142
|
+
// We snapshot the knowledge directory before and after a language flip;
|
|
143
|
+
// the existing files (if any) must remain identical and only NEW
|
|
144
|
+
// commits should produce output under the new language.
|
|
145
|
+
const knowledgePath = readBack.body.project.config.knowledgePath;
|
|
146
|
+
fs.mkdirSync(knowledgePath, { recursive: true });
|
|
147
|
+
const marker = path.join(knowledgePath, 'marker.txt');
|
|
148
|
+
fs.writeFileSync(marker, 'do-not-touch\n');
|
|
149
|
+
const mtimeBefore = fs.statSync(marker).mtimeMs;
|
|
150
|
+
const contentBefore = fs.readFileSync(marker, 'utf8');
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
152
|
+
r = await serverFetch(`http://127.0.0.1:${sitePort}/api/projects/${PROJECT_ID}`, {
|
|
153
|
+
method: 'PATCH', headers: { 'content-type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ knowledgeLanguage: 'zh-CN' }),
|
|
155
|
+
});
|
|
156
|
+
assert.strictEqual(r.status, 200);
|
|
157
|
+
assert(fs.existsSync(marker), 'historical knowledge files must not be deleted by a language change');
|
|
158
|
+
assert.strictEqual(fs.readFileSync(marker, 'utf8'), contentBefore, 'historical content must remain intact');
|
|
159
|
+
assert.strictEqual(fs.statSync(marker).mtimeMs, mtimeBefore, 'historical files must not be touched');
|
|
160
|
+
} finally {
|
|
161
|
+
server.child.kill();
|
|
162
|
+
await new Promise(resolve => server.child.once('exit', resolve));
|
|
163
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
164
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
console.log('knowledge-language-control-test PASS');
|
|
167
|
+
})().catch(error => {
|
|
168
|
+
console.error(error.stack || error.message);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { ProjectRegistryStore } = require('../lib/project-registry-store');
|
|
7
|
+
const { ProjectStore } = require('../lib/project-store');
|
|
8
|
+
const { IndexService } = require('../lib/index-service');
|
|
9
|
+
|
|
10
|
+
(async () => {
|
|
11
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-maintenance-v2-'));
|
|
12
|
+
const layout = new StorageLayout({ dataDir: path.join(temp, 'data') });
|
|
13
|
+
const registry = new ProjectRegistryStore({ layout });
|
|
14
|
+
const projects = new ProjectStore({ layout });
|
|
15
|
+
await registry.initialize();
|
|
16
|
+
const target = layout.getIndexPath();
|
|
17
|
+
fs.mkdirSync(target, { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(target, 'version.txt'), 'old-index', 'utf8');
|
|
19
|
+
const adapter = {
|
|
20
|
+
validationOk: false,
|
|
21
|
+
async buildFull({ targetPath }) {
|
|
22
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(targetPath, 'version.txt'), 'new-index', 'utf8');
|
|
24
|
+
},
|
|
25
|
+
async validateIndex(candidate) {
|
|
26
|
+
return { ok: this.validationOk, marker: fs.readFileSync(path.join(candidate, 'version.txt'), 'utf8') };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const service = new IndexService({ layout, registryStore: registry, projectStore: projects, adapter });
|
|
30
|
+
await assert.rejects(service.fullRebuild({ operationId: 'op-validation-failure' }), error => error.code === 'DATA_CORRUPT');
|
|
31
|
+
assert.strictEqual(fs.readFileSync(path.join(target, 'version.txt'), 'utf8'), 'old-index', 'failed validation must leave the live index untouched');
|
|
32
|
+
assert(!fs.existsSync(path.join(path.dirname(target), '.knowledge.rebuild.op-validation-failure')));
|
|
33
|
+
|
|
34
|
+
adapter.validationOk = true;
|
|
35
|
+
const rebuilt = await service.fullRebuild({ operationId: 'op-valid-rebuild' });
|
|
36
|
+
assert.strictEqual(fs.readFileSync(path.join(target, 'version.txt'), 'utf8'), 'new-index');
|
|
37
|
+
assert(rebuilt.backup && fs.existsSync(rebuilt.backup));
|
|
38
|
+
assert.strictEqual(fs.readFileSync(path.join(rebuilt.backup, 'version.txt'), 'utf8'), 'old-index', 'atomic swap must retain the previous live index');
|
|
39
|
+
console.log('knowledge-maintenance-test: PASS');
|
|
40
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { ProjectStore } = require('../lib/project-store');
|
|
8
|
+
const { MigrationService } = require('../lib/migration-service');
|
|
9
|
+
const { KnowledgeDatabase } = require('../lib/knowledge-db');
|
|
10
|
+
const { EMBEDDING_DIMENSIONS } = require('../lib/knowledge-schema');
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'knowledge-migration-v2-'));
|
|
14
|
+
const oldKnowledgeRoot = path.join(dataDir, 'old-knowledge');
|
|
15
|
+
const fixedKnowledgePath = path.join(oldKnowledgeRoot, 'api-fixed');
|
|
16
|
+
const legacyIndex = path.join(dataDir, 'knowledge.lancedb');
|
|
17
|
+
fs.mkdirSync(fixedKnowledgePath, { recursive: true });
|
|
18
|
+
fs.writeFileSync(path.join(fixedKnowledgePath, 'GOAL.md'), '# API goal\n\nAuthoritative Markdown remains external.\n', 'utf8');
|
|
19
|
+
fs.writeFileSync(path.join(dataDir, 'projects.json'), JSON.stringify({
|
|
20
|
+
api: {
|
|
21
|
+
displayName: 'API', localPath: dataDir, gitPath: dataDir, kbPath: fixedKnowledgePath,
|
|
22
|
+
trackingStartCommit: 'track-before', lastAnalyzedCommit: 'commit-before', enabled: true,
|
|
23
|
+
},
|
|
24
|
+
}, null, 2));
|
|
25
|
+
fs.writeFileSync(path.join(dataDir, 'knowledge-store.json'), JSON.stringify({ rootPath: oldKnowledgeRoot, configured: true }));
|
|
26
|
+
fs.writeFileSync(path.join(dataDir, 'ai-profiles.json'), JSON.stringify({ schema: 'ai-profiles/v1', profiles: [{ id: 'primary', apiKey: 'migration-secret-value' }] }));
|
|
27
|
+
fs.writeFileSync(path.join(dataDir, 'logging.json'), JSON.stringify({ levels: ['debug', 'info', 'error'], retentionDays: 0, maxTotalSizeMB: 128 }));
|
|
28
|
+
fs.writeFileSync(path.join(dataDir, '.hook-trigger-errors.log'), '2026-01-01 legacy hook error\n', 'utf8');
|
|
29
|
+
|
|
30
|
+
const vector = new Array(EMBEDDING_DIMENSIONS).fill(0);
|
|
31
|
+
vector[9] = 1;
|
|
32
|
+
const oldDatabase = new KnowledgeDatabase({ dbPath: legacyIndex });
|
|
33
|
+
await oldDatabase.replaceEntry('project:api', 'GOAL.md', [{ chunkOrder: 0, title: 'Legacy index', chunkText: 'A migrated derived row.', vector }]);
|
|
34
|
+
await oldDatabase.close();
|
|
35
|
+
const oldIndexRows = await (async () => {
|
|
36
|
+
const db = new KnowledgeDatabase({ dbPath: legacyIndex });
|
|
37
|
+
try { return await db.count(); } finally { await db.close(); }
|
|
38
|
+
})();
|
|
39
|
+
|
|
40
|
+
const layout = new StorageLayout({ dataDir });
|
|
41
|
+
const migration = new MigrationService({
|
|
42
|
+
layout,
|
|
43
|
+
legacyDataDir: dataDir,
|
|
44
|
+
indexValidator: async candidate => {
|
|
45
|
+
const db = new KnowledgeDatabase({ dbPath: candidate });
|
|
46
|
+
try { await db.open(); return { ok: (await db.count()) === oldIndexRows }; }
|
|
47
|
+
finally { await db.close(); }
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const result = await migration.migrateIfNeeded({ migrationRunId: 'knowledge-index-v2' });
|
|
51
|
+
assert(result.ok && result.completed && result.migrated, JSON.stringify(result));
|
|
52
|
+
const projectId = result.projectMap.api;
|
|
53
|
+
const projectStore = new ProjectStore({ layout });
|
|
54
|
+
const config = projectStore.readConfig(projectId);
|
|
55
|
+
const state = projectStore.readState(projectId);
|
|
56
|
+
assert.strictEqual(config.knowledgePath, fixedKnowledgePath, 'legacy kbPath must be preserved exactly');
|
|
57
|
+
assert.strictEqual(state.trackingStartCommit, 'track-before');
|
|
58
|
+
assert.strictEqual(state.lastAnalyzedCommit, 'commit-before');
|
|
59
|
+
assert.strictEqual(new SettingsStore({ layout }).read().ai.profiles[0].apiKey, 'migration-secret-value');
|
|
60
|
+
const migratedDatabase = new KnowledgeDatabase({ dbPath: layout.getIndexPath() });
|
|
61
|
+
assert.strictEqual(await migratedDatabase.count(), 1, 'the validated legacy index should be copied to the one internal index path');
|
|
62
|
+
await migratedDatabase.close();
|
|
63
|
+
assert(fs.existsSync(legacyIndex), 'legacy index source must remain for rollback');
|
|
64
|
+
assert(fs.existsSync(path.join(result.recoveryDir, 'backup', 'knowledge.lancedb')), 'recovery must retain an index backup');
|
|
65
|
+
assert(fs.existsSync(path.join(dataDir, '.hook-trigger-errors.log')), 'legacy hook logs must remain at the source');
|
|
66
|
+
assert(fs.existsSync(path.join(result.recoveryDir, 'backup', '.hook-trigger-errors.log')));
|
|
67
|
+
|
|
68
|
+
const settings = new SettingsStore({ layout });
|
|
69
|
+
await settings.updatePatch({ knowledge: { rootPath: path.join(dataDir, 'new-knowledge-root') } });
|
|
70
|
+
assert.strictEqual(projectStore.readConfig(projectId).knowledgePath, fixedKnowledgePath, 'changing the global root must not move an imported project');
|
|
71
|
+
assert.strictEqual(layout.getIndexPath(), path.join(dataDir, 'index', 'knowledge.lancedb'), 'global root changes must not move the derived index');
|
|
72
|
+
const second = await migration.migrateIfNeeded();
|
|
73
|
+
assert.strictEqual(second.migrated, false);
|
|
74
|
+
console.log('knowledge-migration-test: PASS');
|
|
75
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|