@sanqianx/project-knowledge 4.7.0 → 4.8.0
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 +17 -17
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +817 -799
- package/README.md +48 -36
- package/_modules/knowledge-engine/README.md +88 -0
- package/_modules/knowledge-engine/package.json +38 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
- package/_modules/knowledge-engine/src/bin.js +52 -0
- package/_modules/knowledge-engine/src/bridge-client.js +24 -0
- package/_modules/knowledge-engine/src/coordinator.js +255 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
- package/_modules/knowledge-engine/src/memories.js +85 -0
- package/_modules/knowledge-engine/src/migration.js +88 -0
- package/_modules/knowledge-engine/src/retrieval.js +116 -0
- package/_modules/knowledge-engine/src/runtime/README.md +7 -0
- package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
- package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
- package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
- package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
- package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
- package/_modules/knowledge-engine/src/server.js +155 -0
- package/_modules/knowledge-engine/src/store.js +239 -0
- package/_modules/knowledge-engine/ui/app.css +172 -0
- package/_modules/knowledge-engine/ui/app.js +297 -0
- package/_modules/knowledge-engine/ui/index.html +95 -0
- package/_site/README.md +30 -30
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- 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/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +62 -62
- package/_site/_test/claude-workbench-test.js +170 -170
- package/_site/_test/codex-conversation-projection-test.js +2 -1
- package/_site/_test/commit-conversation-binding-test.js +95 -95
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/conversation-api-test.js +109 -109
- package/_site/_test/conversation-store-test.js +136 -136
- package/_site/_test/data-dir-migration-test.js +57 -57
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/make-git-repos.js +12 -1
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/full-integration-e2e-test.js +165 -165
- package/_site/_test/git-validation-test.js +2 -2
- 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/import-preflight-api-test.js +234 -233
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +171 -171
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/knowledge-language-control-test.js +170 -170
- 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-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/legacy-forward-compat-test.js +257 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +363 -363
- 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 +106 -104
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/module-artifact-integrity-test.js +27 -0
- package/_site/_test/module-boundary-test.js +30 -0
- package/_site/_test/module-bridge-eventbridge-test.js +7 -0
- package/_site/_test/module-model-configuration-test.js +43 -0
- package/_site/_test/module-process-lifecycle-test.js +24 -0
- package/_site/_test/module-stream-proxy-test.js +34 -0
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/p0-data-migration-characterization-test.js +37 -37
- package/_site/_test/p0-e2e-gate-test.js +347 -347
- package/_site/_test/packaged-ui-smoke-test.js +11 -9
- 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/product-diagnostics-ui-test.js +75 -0
- package/_site/_test/product-import-ui-test.js +92 -0
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-layout-v2-migration-test.js +127 -127
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- 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/release-version-sync-test.js +1 -1
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +159 -156
- package/_site/_test/server-runtime-migration-safety-test.js +27 -27
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/startup-analysis-disabled-test.js +10 -10
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/tracking-start-test.js +108 -108
- package/_site/_test/v4122-upgrade-data-contract-test.js +56 -56
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/workspace-ui-contract-test.js +28 -43
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-workspace.js +101 -101
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +1 -0
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/claude-cli-runner.js +1510 -1510
- package/_site/lib/commit-conversation-binder.js +234 -234
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +406 -406
- package/_site/lib/contracts.js +284 -279
- package/_site/lib/conversation-query-service.js +144 -144
- package/_site/lib/conversation-store.js +385 -385
- package/_site/lib/data-dir.js +74 -74
- package/_site/lib/data-state-classifier.js +40 -40
- package/_site/lib/engine-cutover.js +97 -0
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +21 -21
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1172 -1172
- package/_site/lib/hook-manager.js +203 -202
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-installer.js +907 -907
- package/_site/lib/kb-framework.js +176 -176
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-tool-runtime.js +359 -359
- package/_site/lib/legacy-data-manifest.js +26 -26
- package/_site/lib/llm-client.js +161 -161
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/migration-service.js +442 -442
- package/_site/lib/module-bridge.js +198 -33
- package/_site/lib/post-commit-automation.js +87 -87
- package/_site/lib/project-lifecycle-service.js +519 -497
- package/_site/lib/project-store.js +268 -262
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-endpoint.js +155 -155
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1590 -1520
- package/_site/lib/settings-store.js +133 -113
- package/_site/lib/storage-layout.js +162 -162
- package/_site/lib/structured-logger.js +574 -574
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +157 -156
- package/_site/scripts/install-module-candidates.js +69 -0
- package/_site/scripts/pack-module-candidates.js +72 -0
- package/_site/scripts/sync-release-version.js +2 -2
- package/_site/scripts/vendor-modules.js +47 -0
- package/_site/scripts/verify-module-runtime.js +45 -0
- package/_site/scripts/verify-product-runtime.js +34 -0
- package/bin/project-knowledge-mcp.js +194 -194
- package/docs/README.zh-CN.md +34 -25
- package/docs/project-registry-schema.md +22 -22
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +220 -0
- package/package.json +13 -9
- 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/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/project-readme.md +34 -34
- package/ui/favicon.svg +38 -38
- package/ui/index.html +51 -149
- package/ui/product.css +9 -0
- package/ui/product.js +386 -0
- package/vendor-manifest.json +46 -0
- package/_site/_test/import-ui-flow-test.js +0 -171
- package/_site/_test/project-control-panel-task14-test.js +0 -63
- package/_site/_test/task15-20-ui-flow-test.js +0 -148
- package/_site/_test/ui-i18n-toggle-test.js +0 -114
- package/_site/_test/ui-smoke-test.js +0 -73
- package/ui/app.css +0 -58
- package/ui/app.js +0 -607
- package/ui/i18n.js +0 -146
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const AtomicFile = require('./atomic-file');
|
|
4
|
-
const { DomainError, createId, validateProjectId } = require('./contracts');
|
|
5
|
-
const { StorageLayout } = require('./storage-layout');
|
|
6
|
-
const { ProjectRegistryStore } = require('./project-registry-store');
|
|
7
|
-
const { ProjectStore } = require('./project-store');
|
|
8
|
-
const { CHUNKER_VERSION, listMarkdownFiles } = require('./markdown-knowledge-indexer');
|
|
9
|
-
const { sha256 } = require('./knowledge-schema');
|
|
10
|
-
|
|
11
|
-
let globalWriterTail = Promise.resolve();
|
|
12
|
-
|
|
13
|
-
function buildSourceManifest(projectId, knowledgePath, generation, sourceCommit = '') {
|
|
14
|
-
const root = path.resolve(knowledgePath);
|
|
15
|
-
const entries = {};
|
|
16
|
-
for (const filePath of listMarkdownFiles(root)) {
|
|
17
|
-
const entryId = path.relative(root, filePath).replace(/\\/g, '/');
|
|
18
|
-
const markdown = fs.readFileSync(filePath, 'utf8');
|
|
19
|
-
entries[entryId] = {
|
|
20
|
-
entryId,
|
|
21
|
-
documentHash: sha256(`chunker:${CHUNKER_VERSION}\n${markdown}`),
|
|
22
|
-
sourceCommit: sourceCommit || '',
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
schema: 'knowledge-index-source-manifest/v1',
|
|
27
|
-
projectId,
|
|
28
|
-
generation: Number(generation || 0),
|
|
29
|
-
sourceCommit: sourceCommit || null,
|
|
30
|
-
entries,
|
|
31
|
-
entryCount: Object.keys(entries).length,
|
|
32
|
-
updatedAt: new Date().toISOString(),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function enqueueGlobal(task) {
|
|
37
|
-
const run = globalWriterTail.catch(() => {}).then(task);
|
|
38
|
-
globalWriterTail = run.catch(() => {});
|
|
39
|
-
return run;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
class IndexService {
|
|
43
|
-
constructor(options = {}) {
|
|
44
|
-
this.layout = options.layout || new StorageLayout(options);
|
|
45
|
-
this.registryStore = options.registryStore || new ProjectRegistryStore({ layout: this.layout });
|
|
46
|
-
this.projectStore = options.projectStore || new ProjectStore({ layout: this.layout });
|
|
47
|
-
this.adapter = options.adapter || null;
|
|
48
|
-
this.atomic = options.atomic || AtomicFile;
|
|
49
|
-
this.logger = options.logger || null;
|
|
50
|
-
this.pending = new Map();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async log(level, event, message, context) {
|
|
54
|
-
if (this.logger && typeof this.logger[level] === 'function') await this.logger[level](event, message, context);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
enqueue(projectId) {
|
|
58
|
-
validateProjectId(projectId);
|
|
59
|
-
const existing = this.pending.get(projectId);
|
|
60
|
-
if (existing) {
|
|
61
|
-
existing.rerun = true;
|
|
62
|
-
return existing.promise;
|
|
63
|
-
}
|
|
64
|
-
const entry = { rerun: false, promise: null };
|
|
65
|
-
entry.promise = enqueueGlobal(() => this.processEntry(projectId, entry));
|
|
66
|
-
this.pending.set(projectId, entry);
|
|
67
|
-
entry.promise.finally(() => {
|
|
68
|
-
if (this.pending.get(projectId) === entry) this.pending.delete(projectId);
|
|
69
|
-
});
|
|
70
|
-
return entry.promise;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async processEntry(projectId, entry) {
|
|
74
|
-
let lastResult = { ok: true, skipped: true };
|
|
75
|
-
do {
|
|
76
|
-
entry.rerun = false;
|
|
77
|
-
lastResult = await this.processProject(projectId);
|
|
78
|
-
if (!lastResult.ok) return lastResult;
|
|
79
|
-
const state = this.projectStore.readState(projectId);
|
|
80
|
-
if (state.index.dirty) entry.rerun = true;
|
|
81
|
-
} while (entry.rerun);
|
|
82
|
-
return lastResult;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async processProject(projectId) {
|
|
86
|
-
const config = this.projectStore.readConfig(projectId);
|
|
87
|
-
const before = this.projectStore.readState(projectId);
|
|
88
|
-
if (!before.index.dirty) return { ok: true, skipped: true, projectId };
|
|
89
|
-
const generation = before.index.generation;
|
|
90
|
-
const operationId = createId('op');
|
|
91
|
-
try {
|
|
92
|
-
if (!this.adapter || typeof this.adapter.indexProject !== 'function') throw new DomainError('INVALID_ARGUMENT', 'Index adapter is unavailable.', { status: 503, retryable: true });
|
|
93
|
-
const result = await this.adapter.indexProject({
|
|
94
|
-
projectId,
|
|
95
|
-
config,
|
|
96
|
-
knowledgePath: config.knowledgePath,
|
|
97
|
-
indexPath: this.layout.getIndexPath(),
|
|
98
|
-
generation,
|
|
99
|
-
sinceCommit: before.index.sinceCommit,
|
|
100
|
-
});
|
|
101
|
-
const sourceManifest = buildSourceManifest(projectId, config.knowledgePath, generation, before.index.sinceCommit);
|
|
102
|
-
this.atomic.writeJsonAtomic(this.layout.getRuntimePath('index-sources', `${projectId}.json`), sourceManifest);
|
|
103
|
-
await this.projectStore.updateState(projectId, state => {
|
|
104
|
-
if (state.index.generation === generation) {
|
|
105
|
-
state.index.dirty = false;
|
|
106
|
-
state.index.sinceCommit = null;
|
|
107
|
-
state.index.lastError = null;
|
|
108
|
-
state.index.lastIndexedAt = new Date().toISOString();
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
await this.log('info', 'index.project_applied', 'Derived knowledge index updated.', { projectId, operationId, generation, phase: 'index.applied' });
|
|
112
|
-
return { ok: true, projectId, generation, result: result || null };
|
|
113
|
-
} catch (error) {
|
|
114
|
-
await this.projectStore.updateState(projectId, state => {
|
|
115
|
-
state.index.dirty = true;
|
|
116
|
-
state.index.lastError = {
|
|
117
|
-
code: error.code || 'INVALID_ARGUMENT',
|
|
118
|
-
message: String(error.message || 'Index update failed.'),
|
|
119
|
-
generation,
|
|
120
|
-
ts: new Date().toISOString(),
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
await this.log('error', 'index.project_failed', 'Derived index update failed; Markdown remains authoritative.', { projectId, operationId, generation, phase: 'index.queued', error });
|
|
124
|
-
return { ok: false, projectId, generation, error: { code: error.code || 'INVALID_ARGUMENT', message: error.message } };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
retryDirtyProjects() {
|
|
129
|
-
return Promise.all(this.registryStore.listIds()
|
|
130
|
-
.filter(projectId => this.projectStore.readState(projectId).index.dirty)
|
|
131
|
-
.map(projectId => this.enqueue(projectId)));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async fullRebuild(options = {}) {
|
|
135
|
-
if (!this.adapter || typeof this.adapter.buildFull !== 'function' || typeof this.adapter.validateIndex !== 'function') {
|
|
136
|
-
throw new DomainError('INVALID_ARGUMENT', 'Index adapter does not support atomic full rebuild.', { status: 503 });
|
|
137
|
-
}
|
|
138
|
-
return enqueueGlobal(async () => {
|
|
139
|
-
const operationId = options.operationId || createId('op');
|
|
140
|
-
const target = this.layout.getIndexPath();
|
|
141
|
-
const parent = path.dirname(target);
|
|
142
|
-
const temp = path.join(parent, `.knowledge.rebuild.${operationId}`);
|
|
143
|
-
const backup = this.layout.getRecoveryPath('index', `${operationId}.previous`);
|
|
144
|
-
if (fs.existsSync(temp)) throw new DomainError('MIGRATION_TARGET_CONFLICT', 'Index rebuild staging already exists.', { status: 409 });
|
|
145
|
-
const projects = this.registryStore.listIds().map(projectId => ({ projectId, config: this.projectStore.readConfig(projectId) }));
|
|
146
|
-
try {
|
|
147
|
-
await this.adapter.buildFull({ targetPath: temp, projects });
|
|
148
|
-
const validation = await this.adapter.validateIndex(temp);
|
|
149
|
-
if (!validation || validation.ok !== true) throw new DomainError('DATA_CORRUPT', 'Rebuilt index validation failed.', { status: 500 });
|
|
150
|
-
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
151
|
-
if (fs.existsSync(target)) fs.renameSync(target, backup);
|
|
152
|
-
try { fs.renameSync(temp, target); }
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (fs.existsSync(backup) && !fs.existsSync(target)) fs.renameSync(backup, target);
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
for (const { projectId, config } of projects) {
|
|
158
|
-
const state = this.projectStore.readState(projectId);
|
|
159
|
-
const sourceManifest = buildSourceManifest(projectId, config.knowledgePath, state.index.generation, state.index.sinceCommit);
|
|
160
|
-
this.atomic.writeJsonAtomic(this.layout.getRuntimePath('index-sources', `${projectId}.json`), sourceManifest);
|
|
161
|
-
}
|
|
162
|
-
return { ok: true, operationId, target, backup: fs.existsSync(backup) ? backup : null, validation };
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (fs.existsSync(temp)) fs.rmSync(temp, { recursive: true, force: true });
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
static async flush() { await globalWriterTail.catch(() => {}); }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
module.exports = { IndexService, enqueueGlobal, buildSourceManifest };
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const AtomicFile = require('./atomic-file');
|
|
4
|
+
const { DomainError, createId, validateProjectId } = require('./contracts');
|
|
5
|
+
const { StorageLayout } = require('./storage-layout');
|
|
6
|
+
const { ProjectRegistryStore } = require('./project-registry-store');
|
|
7
|
+
const { ProjectStore } = require('./project-store');
|
|
8
|
+
const { CHUNKER_VERSION, listMarkdownFiles } = require('./markdown-knowledge-indexer');
|
|
9
|
+
const { sha256 } = require('./knowledge-schema');
|
|
10
|
+
|
|
11
|
+
let globalWriterTail = Promise.resolve();
|
|
12
|
+
|
|
13
|
+
function buildSourceManifest(projectId, knowledgePath, generation, sourceCommit = '') {
|
|
14
|
+
const root = path.resolve(knowledgePath);
|
|
15
|
+
const entries = {};
|
|
16
|
+
for (const filePath of listMarkdownFiles(root)) {
|
|
17
|
+
const entryId = path.relative(root, filePath).replace(/\\/g, '/');
|
|
18
|
+
const markdown = fs.readFileSync(filePath, 'utf8');
|
|
19
|
+
entries[entryId] = {
|
|
20
|
+
entryId,
|
|
21
|
+
documentHash: sha256(`chunker:${CHUNKER_VERSION}\n${markdown}`),
|
|
22
|
+
sourceCommit: sourceCommit || '',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
schema: 'knowledge-index-source-manifest/v1',
|
|
27
|
+
projectId,
|
|
28
|
+
generation: Number(generation || 0),
|
|
29
|
+
sourceCommit: sourceCommit || null,
|
|
30
|
+
entries,
|
|
31
|
+
entryCount: Object.keys(entries).length,
|
|
32
|
+
updatedAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function enqueueGlobal(task) {
|
|
37
|
+
const run = globalWriterTail.catch(() => {}).then(task);
|
|
38
|
+
globalWriterTail = run.catch(() => {});
|
|
39
|
+
return run;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class IndexService {
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
this.layout = options.layout || new StorageLayout(options);
|
|
45
|
+
this.registryStore = options.registryStore || new ProjectRegistryStore({ layout: this.layout });
|
|
46
|
+
this.projectStore = options.projectStore || new ProjectStore({ layout: this.layout });
|
|
47
|
+
this.adapter = options.adapter || null;
|
|
48
|
+
this.atomic = options.atomic || AtomicFile;
|
|
49
|
+
this.logger = options.logger || null;
|
|
50
|
+
this.pending = new Map();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async log(level, event, message, context) {
|
|
54
|
+
if (this.logger && typeof this.logger[level] === 'function') await this.logger[level](event, message, context);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
enqueue(projectId) {
|
|
58
|
+
validateProjectId(projectId);
|
|
59
|
+
const existing = this.pending.get(projectId);
|
|
60
|
+
if (existing) {
|
|
61
|
+
existing.rerun = true;
|
|
62
|
+
return existing.promise;
|
|
63
|
+
}
|
|
64
|
+
const entry = { rerun: false, promise: null };
|
|
65
|
+
entry.promise = enqueueGlobal(() => this.processEntry(projectId, entry));
|
|
66
|
+
this.pending.set(projectId, entry);
|
|
67
|
+
entry.promise.finally(() => {
|
|
68
|
+
if (this.pending.get(projectId) === entry) this.pending.delete(projectId);
|
|
69
|
+
});
|
|
70
|
+
return entry.promise;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async processEntry(projectId, entry) {
|
|
74
|
+
let lastResult = { ok: true, skipped: true };
|
|
75
|
+
do {
|
|
76
|
+
entry.rerun = false;
|
|
77
|
+
lastResult = await this.processProject(projectId);
|
|
78
|
+
if (!lastResult.ok) return lastResult;
|
|
79
|
+
const state = this.projectStore.readState(projectId);
|
|
80
|
+
if (state.index.dirty) entry.rerun = true;
|
|
81
|
+
} while (entry.rerun);
|
|
82
|
+
return lastResult;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async processProject(projectId) {
|
|
86
|
+
const config = this.projectStore.readConfig(projectId);
|
|
87
|
+
const before = this.projectStore.readState(projectId);
|
|
88
|
+
if (!before.index.dirty) return { ok: true, skipped: true, projectId };
|
|
89
|
+
const generation = before.index.generation;
|
|
90
|
+
const operationId = createId('op');
|
|
91
|
+
try {
|
|
92
|
+
if (!this.adapter || typeof this.adapter.indexProject !== 'function') throw new DomainError('INVALID_ARGUMENT', 'Index adapter is unavailable.', { status: 503, retryable: true });
|
|
93
|
+
const result = await this.adapter.indexProject({
|
|
94
|
+
projectId,
|
|
95
|
+
config,
|
|
96
|
+
knowledgePath: config.knowledgePath,
|
|
97
|
+
indexPath: this.layout.getIndexPath(),
|
|
98
|
+
generation,
|
|
99
|
+
sinceCommit: before.index.sinceCommit,
|
|
100
|
+
});
|
|
101
|
+
const sourceManifest = buildSourceManifest(projectId, config.knowledgePath, generation, before.index.sinceCommit);
|
|
102
|
+
this.atomic.writeJsonAtomic(this.layout.getRuntimePath('index-sources', `${projectId}.json`), sourceManifest);
|
|
103
|
+
await this.projectStore.updateState(projectId, state => {
|
|
104
|
+
if (state.index.generation === generation) {
|
|
105
|
+
state.index.dirty = false;
|
|
106
|
+
state.index.sinceCommit = null;
|
|
107
|
+
state.index.lastError = null;
|
|
108
|
+
state.index.lastIndexedAt = new Date().toISOString();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
await this.log('info', 'index.project_applied', 'Derived knowledge index updated.', { projectId, operationId, generation, phase: 'index.applied' });
|
|
112
|
+
return { ok: true, projectId, generation, result: result || null };
|
|
113
|
+
} catch (error) {
|
|
114
|
+
await this.projectStore.updateState(projectId, state => {
|
|
115
|
+
state.index.dirty = true;
|
|
116
|
+
state.index.lastError = {
|
|
117
|
+
code: error.code || 'INVALID_ARGUMENT',
|
|
118
|
+
message: String(error.message || 'Index update failed.'),
|
|
119
|
+
generation,
|
|
120
|
+
ts: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
await this.log('error', 'index.project_failed', 'Derived index update failed; Markdown remains authoritative.', { projectId, operationId, generation, phase: 'index.queued', error });
|
|
124
|
+
return { ok: false, projectId, generation, error: { code: error.code || 'INVALID_ARGUMENT', message: error.message } };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
retryDirtyProjects() {
|
|
129
|
+
return Promise.all(this.registryStore.listIds()
|
|
130
|
+
.filter(projectId => this.projectStore.readState(projectId).index.dirty)
|
|
131
|
+
.map(projectId => this.enqueue(projectId)));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async fullRebuild(options = {}) {
|
|
135
|
+
if (!this.adapter || typeof this.adapter.buildFull !== 'function' || typeof this.adapter.validateIndex !== 'function') {
|
|
136
|
+
throw new DomainError('INVALID_ARGUMENT', 'Index adapter does not support atomic full rebuild.', { status: 503 });
|
|
137
|
+
}
|
|
138
|
+
return enqueueGlobal(async () => {
|
|
139
|
+
const operationId = options.operationId || createId('op');
|
|
140
|
+
const target = this.layout.getIndexPath();
|
|
141
|
+
const parent = path.dirname(target);
|
|
142
|
+
const temp = path.join(parent, `.knowledge.rebuild.${operationId}`);
|
|
143
|
+
const backup = this.layout.getRecoveryPath('index', `${operationId}.previous`);
|
|
144
|
+
if (fs.existsSync(temp)) throw new DomainError('MIGRATION_TARGET_CONFLICT', 'Index rebuild staging already exists.', { status: 409 });
|
|
145
|
+
const projects = this.registryStore.listIds().map(projectId => ({ projectId, config: this.projectStore.readConfig(projectId) }));
|
|
146
|
+
try {
|
|
147
|
+
await this.adapter.buildFull({ targetPath: temp, projects });
|
|
148
|
+
const validation = await this.adapter.validateIndex(temp);
|
|
149
|
+
if (!validation || validation.ok !== true) throw new DomainError('DATA_CORRUPT', 'Rebuilt index validation failed.', { status: 500 });
|
|
150
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
151
|
+
if (fs.existsSync(target)) fs.renameSync(target, backup);
|
|
152
|
+
try { fs.renameSync(temp, target); }
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (fs.existsSync(backup) && !fs.existsSync(target)) fs.renameSync(backup, target);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
for (const { projectId, config } of projects) {
|
|
158
|
+
const state = this.projectStore.readState(projectId);
|
|
159
|
+
const sourceManifest = buildSourceManifest(projectId, config.knowledgePath, state.index.generation, state.index.sinceCommit);
|
|
160
|
+
this.atomic.writeJsonAtomic(this.layout.getRuntimePath('index-sources', `${projectId}.json`), sourceManifest);
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, operationId, target, backup: fs.existsSync(backup) ? backup : null, validation };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (fs.existsSync(temp)) fs.rmSync(temp, { recursive: true, force: true });
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static async flush() { await globalWriterTail.catch(() => {}); }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { IndexService, enqueueGlobal, buildSourceManifest };
|