simple-dynamsoft-mcp 7.2.0 → 7.2.2
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/.env.example +5 -3
- package/README.md +1 -2
- package/package.json +3 -2
- package/src/data/download-utils.js +11 -3
- package/src/index.js +1 -2
- package/src/rag/config.js +16 -3
- package/src/rag/gemini-retry.js +1 -1
- package/src/rag/index.js +6 -2
- package/src/rag/logger.js +4 -19
- package/src/rag/providers.js +15 -46
- package/src/rag/search-utils.js +8 -1
- package/src/rag/vector-cache.js +173 -171
- package/scripts/compute-repo-signatures.mjs +0 -210
- package/scripts/data-sync-azure.mjs +0 -364
- package/src/data/shared-state.js +0 -214
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
SHARED_STATE_SCHEMA_VERSION,
|
|
8
|
-
computeRepoSignature,
|
|
9
|
-
createSharedState,
|
|
10
|
-
loadSharedState,
|
|
11
|
-
normalizeRepoKey,
|
|
12
|
-
normalizeRepoPath
|
|
13
|
-
} from "../src/data/shared-state.js";
|
|
14
|
-
|
|
15
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const projectRoot = join(__dirname, "..");
|
|
17
|
-
const DEFAULT_EMBEDDING_MODEL = "models/gemini-embedding-001";
|
|
18
|
-
const DEFAULT_INDEX_VERSION = "azure-shared-v1";
|
|
19
|
-
const DEFAULT_INDEX_CONFIG = {
|
|
20
|
-
chunkSize: 1200,
|
|
21
|
-
chunkOverlap: 200,
|
|
22
|
-
maxChunksPerDoc: 6,
|
|
23
|
-
maxTextChars: 4000
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function parseIntegerOption(flag, rawValue, { min = 0 } = {}) {
|
|
27
|
-
const text = String(rawValue ?? "").trim();
|
|
28
|
-
if (!/^-?\d+$/.test(text)) {
|
|
29
|
-
throw new Error(`Invalid ${flag}: expected an integer, received '${rawValue}'`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const value = Number.parseInt(text, 10);
|
|
33
|
-
if (!Number.isSafeInteger(value) || value < min) {
|
|
34
|
-
throw new Error(`Invalid ${flag}: expected integer >= ${min}, received '${rawValue}'`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function toAbsolutePath(pathValue) {
|
|
41
|
-
if (!pathValue) return "";
|
|
42
|
-
if (pathValue.startsWith("/")) return pathValue;
|
|
43
|
-
return join(projectRoot, pathValue);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseArgs(argv, env = process.env) {
|
|
47
|
-
const currentStatePath = env.DATA_SYNC_AZURE_CURRENT_STATE_PATH || ".tmp/azure-shared-state/state/current.json";
|
|
48
|
-
const defaultIndexConfig = {
|
|
49
|
-
chunkSize: env.DATA_SYNC_AZURE_CHUNK_SIZE
|
|
50
|
-
? parseIntegerOption("--chunk-size", env.DATA_SYNC_AZURE_CHUNK_SIZE, { min: 0 })
|
|
51
|
-
: DEFAULT_INDEX_CONFIG.chunkSize,
|
|
52
|
-
chunkOverlap: env.DATA_SYNC_AZURE_CHUNK_OVERLAP
|
|
53
|
-
? parseIntegerOption("--chunk-overlap", env.DATA_SYNC_AZURE_CHUNK_OVERLAP, { min: 0 })
|
|
54
|
-
: DEFAULT_INDEX_CONFIG.chunkOverlap,
|
|
55
|
-
maxChunksPerDoc: env.DATA_SYNC_AZURE_MAX_CHUNKS_PER_DOC
|
|
56
|
-
? parseIntegerOption("--max-chunks-per-doc", env.DATA_SYNC_AZURE_MAX_CHUNKS_PER_DOC, { min: 1 })
|
|
57
|
-
: DEFAULT_INDEX_CONFIG.maxChunksPerDoc,
|
|
58
|
-
maxTextChars: env.DATA_SYNC_AZURE_MAX_TEXT_CHARS
|
|
59
|
-
? parseIntegerOption("--max-text-chars", env.DATA_SYNC_AZURE_MAX_TEXT_CHARS, { min: 0 })
|
|
60
|
-
: DEFAULT_INDEX_CONFIG.maxTextChars
|
|
61
|
-
};
|
|
62
|
-
const defaults = {
|
|
63
|
-
manifestPath: env.DATA_SYNC_AZURE_MANIFEST_PATH || "data/metadata/data-manifest.json",
|
|
64
|
-
currentStatePath,
|
|
65
|
-
nextStatePath: env.DATA_SYNC_AZURE_NEXT_STATE_PATH || ".tmp/azure-shared-state/state/next-state.json",
|
|
66
|
-
planOutputPath: env.DATA_SYNC_AZURE_PLAN_OUTPUT_PATH || ".tmp/azure-shared-state/state/plan.json",
|
|
67
|
-
embeddingModel: env.DATA_SYNC_AZURE_EMBEDDING_MODEL || DEFAULT_EMBEDDING_MODEL,
|
|
68
|
-
indexVersion: env.DATA_SYNC_AZURE_INDEX_VERSION || DEFAULT_INDEX_VERSION,
|
|
69
|
-
generatedAt: env.DATA_SYNC_AZURE_GENERATED_AT || new Date().toISOString(),
|
|
70
|
-
schemaVersion: SHARED_STATE_SCHEMA_VERSION,
|
|
71
|
-
indexConfig: defaultIndexConfig
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < argv.length; i++) {
|
|
75
|
-
const arg = argv[i];
|
|
76
|
-
const value = argv[i + 1];
|
|
77
|
-
|
|
78
|
-
if (arg === "--manifest" && value) {
|
|
79
|
-
defaults.manifestPath = value;
|
|
80
|
-
i++;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (arg === "--current-state" && value) {
|
|
84
|
-
defaults.currentStatePath = value;
|
|
85
|
-
i++;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (arg === "--next-state" && value) {
|
|
89
|
-
defaults.nextStatePath = value;
|
|
90
|
-
i++;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (arg === "--plan-output" && value) {
|
|
94
|
-
defaults.planOutputPath = value;
|
|
95
|
-
i++;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (arg === "--embedding-model" && value) {
|
|
99
|
-
defaults.embeddingModel = value;
|
|
100
|
-
i++;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (arg === "--index-version" && value) {
|
|
104
|
-
defaults.indexVersion = value;
|
|
105
|
-
i++;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (arg === "--generated-at" && value) {
|
|
109
|
-
defaults.generatedAt = value;
|
|
110
|
-
i++;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (arg === "--schema-version" && value) {
|
|
114
|
-
defaults.schemaVersion = parseIntegerOption("--schema-version", value, { min: 1 });
|
|
115
|
-
i++;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (arg === "--chunk-size" && value) {
|
|
119
|
-
defaults.indexConfig.chunkSize = parseIntegerOption("--chunk-size", value, { min: 0 });
|
|
120
|
-
i++;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (arg === "--chunk-overlap" && value) {
|
|
124
|
-
defaults.indexConfig.chunkOverlap = parseIntegerOption("--chunk-overlap", value, { min: 0 });
|
|
125
|
-
i++;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (arg === "--max-chunks-per-doc" && value) {
|
|
129
|
-
defaults.indexConfig.maxChunksPerDoc = parseIntegerOption("--max-chunks-per-doc", value, { min: 1 });
|
|
130
|
-
i++;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (arg === "--max-text-chars" && value) {
|
|
134
|
-
defaults.indexConfig.maxTextChars = parseIntegerOption("--max-text-chars", value, { min: 0 });
|
|
135
|
-
i++;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
...defaults,
|
|
141
|
-
manifestPath: toAbsolutePath(defaults.manifestPath),
|
|
142
|
-
currentStatePath: toAbsolutePath(defaults.currentStatePath),
|
|
143
|
-
nextStatePath: toAbsolutePath(defaults.nextStatePath),
|
|
144
|
-
planOutputPath: toAbsolutePath(defaults.planOutputPath)
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function readManifest(manifestPath) {
|
|
149
|
-
if (!existsSync(manifestPath)) {
|
|
150
|
-
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
154
|
-
if (!Array.isArray(manifest?.repos)) {
|
|
155
|
-
throw new Error("Manifest must include a repos array");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return manifest;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function buildShardPath(repoPath, signature) {
|
|
162
|
-
return `rag/cache/gemini-${signature}.json`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function buildDesiredRepos(manifestRepos, options) {
|
|
166
|
-
const repos = {};
|
|
167
|
-
const sourcePathByKey = new Map();
|
|
168
|
-
|
|
169
|
-
for (const repo of manifestRepos) {
|
|
170
|
-
const path = normalizeRepoPath(repo.path);
|
|
171
|
-
const key = normalizeRepoKey(path);
|
|
172
|
-
if (!key) continue;
|
|
173
|
-
|
|
174
|
-
const existingPath = sourcePathByKey.get(key);
|
|
175
|
-
if (existingPath && existingPath !== path) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
`Repo key collision for '${key}': '${existingPath}' and '${path}' normalize to the same key`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const signature = computeRepoSignature({
|
|
182
|
-
repo,
|
|
183
|
-
embeddingModel: options.embeddingModel,
|
|
184
|
-
indexConfig: options.indexConfig,
|
|
185
|
-
indexVersion: options.indexVersion,
|
|
186
|
-
schemaVersion: options.schemaVersion
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
repos[key] = {
|
|
190
|
-
path,
|
|
191
|
-
commit: String(repo.commit || "").trim(),
|
|
192
|
-
signature,
|
|
193
|
-
shardPath: buildShardPath(path, signature)
|
|
194
|
-
};
|
|
195
|
-
sourcePathByKey.set(key, path);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return repos;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function loadCurrentState(statePath, { generatedAt, indexVersion } = {}) {
|
|
202
|
-
if (!existsSync(statePath)) {
|
|
203
|
-
return createSharedState({
|
|
204
|
-
generatedAt,
|
|
205
|
-
indexVersion,
|
|
206
|
-
repos: {}
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return loadSharedState(readFileSync(statePath, "utf8"));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function sortStrings(values) {
|
|
214
|
-
return [...values].sort((a, b) => a.localeCompare(b));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function computeRepoDiff({ currentRepos, desiredRepos }) {
|
|
218
|
-
const changed = [];
|
|
219
|
-
const added = [];
|
|
220
|
-
const unchanged = [];
|
|
221
|
-
const removed = [];
|
|
222
|
-
|
|
223
|
-
const currentKeys = new Set(Object.keys(currentRepos || {}));
|
|
224
|
-
const desiredKeys = new Set(Object.keys(desiredRepos || {}));
|
|
225
|
-
|
|
226
|
-
for (const key of desiredKeys) {
|
|
227
|
-
if (!currentKeys.has(key)) {
|
|
228
|
-
added.push(key);
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const currentSignature = String(currentRepos[key]?.signature || "").trim();
|
|
233
|
-
const desiredSignature = String(desiredRepos[key]?.signature || "").trim();
|
|
234
|
-
if (currentSignature === desiredSignature) {
|
|
235
|
-
unchanged.push(key);
|
|
236
|
-
} else {
|
|
237
|
-
changed.push(key);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
for (const key of currentKeys) {
|
|
242
|
-
if (!desiredKeys.has(key)) {
|
|
243
|
-
removed.push(key);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
changed: sortStrings(changed),
|
|
249
|
-
added: sortStrings(added),
|
|
250
|
-
unchanged: sortStrings(unchanged),
|
|
251
|
-
removed: sortStrings(removed),
|
|
252
|
-
hasChanges: changed.length + added.length + removed.length > 0
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function ensureParentDir(filePath) {
|
|
257
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function writeJsonFile(filePath, payload) {
|
|
261
|
-
ensureParentDir(filePath);
|
|
262
|
-
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function simulateAtomicPromotion({ currentStatePath, nextState }) {
|
|
266
|
-
const promotionPath = `${currentStatePath}.next.json`;
|
|
267
|
-
writeJsonFile(promotionPath, nextState);
|
|
268
|
-
renameSync(promotionPath, currentStatePath);
|
|
269
|
-
return {
|
|
270
|
-
promotionPath,
|
|
271
|
-
promotedPath: currentStatePath
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function buildPlan({ options, currentState, nextState, diff }) {
|
|
276
|
-
return {
|
|
277
|
-
kind: "azure_shared_state_sync_plan",
|
|
278
|
-
generatedAt: options.generatedAt,
|
|
279
|
-
manifestPath: options.manifestPath,
|
|
280
|
-
currentStatePath: options.currentStatePath,
|
|
281
|
-
nextStatePath: options.nextStatePath,
|
|
282
|
-
embeddingModel: options.embeddingModel,
|
|
283
|
-
indexVersion: options.indexVersion,
|
|
284
|
-
schemaVersion: options.schemaVersion,
|
|
285
|
-
summary: {
|
|
286
|
-
currentRepos: Object.keys(currentState.repos).length,
|
|
287
|
-
desiredRepos: Object.keys(nextState.repos).length,
|
|
288
|
-
changed: diff.changed.length,
|
|
289
|
-
added: diff.added.length,
|
|
290
|
-
unchanged: diff.unchanged.length,
|
|
291
|
-
removed: diff.removed.length,
|
|
292
|
-
hasChanges: diff.hasChanges
|
|
293
|
-
},
|
|
294
|
-
repos: diff
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function logSummary(plan) {
|
|
299
|
-
const summary = plan.summary;
|
|
300
|
-
console.log(`[data-sync-azure] manifest=${plan.manifestPath}`);
|
|
301
|
-
console.log(`[data-sync-azure] current_state=${plan.currentStatePath}`);
|
|
302
|
-
console.log(`[data-sync-azure] next_state=${plan.nextStatePath}`);
|
|
303
|
-
console.log(
|
|
304
|
-
`[data-sync-azure] repos current=${summary.currentRepos} desired=${summary.desiredRepos} ` +
|
|
305
|
-
`changed=${summary.changed} added=${summary.added} unchanged=${summary.unchanged} removed=${summary.removed}`
|
|
306
|
-
);
|
|
307
|
-
console.log(`[data-sync-azure] has_changes=${summary.hasChanges}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function runDataSyncAzure(argv = process.argv.slice(2), env = process.env) {
|
|
311
|
-
const options = parseArgs(argv, env);
|
|
312
|
-
const manifest = readManifest(options.manifestPath);
|
|
313
|
-
const currentState = loadCurrentState(options.currentStatePath, {
|
|
314
|
-
generatedAt: options.generatedAt,
|
|
315
|
-
indexVersion: options.indexVersion
|
|
316
|
-
});
|
|
317
|
-
const desiredRepos = buildDesiredRepos(manifest.repos, options);
|
|
318
|
-
const nextState = createSharedState({
|
|
319
|
-
generatedAt: options.generatedAt,
|
|
320
|
-
indexVersion: options.indexVersion,
|
|
321
|
-
schemaVersion: options.schemaVersion,
|
|
322
|
-
repos: desiredRepos
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const diff = computeRepoDiff({
|
|
326
|
-
currentRepos: currentState.repos,
|
|
327
|
-
desiredRepos: nextState.repos
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const plan = buildPlan({
|
|
331
|
-
options,
|
|
332
|
-
currentState,
|
|
333
|
-
nextState,
|
|
334
|
-
diff
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
writeJsonFile(options.planOutputPath, plan);
|
|
338
|
-
writeJsonFile(options.nextStatePath, nextState);
|
|
339
|
-
const promotion = simulateAtomicPromotion({
|
|
340
|
-
currentStatePath: options.currentStatePath,
|
|
341
|
-
nextState
|
|
342
|
-
});
|
|
343
|
-
logSummary(plan);
|
|
344
|
-
console.log(`[data-sync-azure] plan_json=${options.planOutputPath}`);
|
|
345
|
-
console.log(`[data-sync-azure] promoted_state=${promotion.promotedPath}`);
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
options,
|
|
349
|
-
plan,
|
|
350
|
-
nextState,
|
|
351
|
-
promotion
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
356
|
-
runDataSyncAzure();
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
export {
|
|
360
|
-
buildDesiredRepos,
|
|
361
|
-
computeRepoDiff,
|
|
362
|
-
parseArgs,
|
|
363
|
-
runDataSyncAzure
|
|
364
|
-
};
|
package/src/data/shared-state.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
const SHARED_STATE_SCHEMA_VERSION = 1;
|
|
4
|
-
|
|
5
|
-
function normalizeRepoPath(input) {
|
|
6
|
-
if (input === undefined || input === null) return "";
|
|
7
|
-
const normalized = String(input)
|
|
8
|
-
.trim()
|
|
9
|
-
.replace(/\\/g, "/")
|
|
10
|
-
.replace(/^\.\//, "")
|
|
11
|
-
.replace(/^\/+/, "")
|
|
12
|
-
.replace(/\/+/g, "/")
|
|
13
|
-
.replace(/\/+$/, "");
|
|
14
|
-
return normalized;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeRepoKey(input) {
|
|
18
|
-
const path = normalizeRepoPath(input).toLowerCase();
|
|
19
|
-
if (!path) return "";
|
|
20
|
-
return path
|
|
21
|
-
.replace(/[^a-z0-9]+/g, "_")
|
|
22
|
-
.replace(/^_+/, "")
|
|
23
|
-
.replace(/_+$/, "");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function stableValue(value) {
|
|
27
|
-
if (Array.isArray(value)) {
|
|
28
|
-
return value.map((item) => stableValue(item));
|
|
29
|
-
}
|
|
30
|
-
if (value && typeof value === "object") {
|
|
31
|
-
const out = {};
|
|
32
|
-
const keys = Object.keys(value).sort((a, b) => a.localeCompare(b));
|
|
33
|
-
for (const key of keys) {
|
|
34
|
-
out[key] = stableValue(value[key]);
|
|
35
|
-
}
|
|
36
|
-
return out;
|
|
37
|
-
}
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function stableStringify(value) {
|
|
42
|
-
return JSON.stringify(stableValue(value));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function normalizeRepoEntry(key, entry) {
|
|
46
|
-
const source = entry && typeof entry === "object" ? entry : {};
|
|
47
|
-
const normalizedPath = normalizeRepoPath(source.path || key);
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
path: normalizedPath,
|
|
51
|
-
commit: String(source.commit || "").trim(),
|
|
52
|
-
signature: String(source.signature || "").trim(),
|
|
53
|
-
shardPath: normalizeShardPath(source.shardPath),
|
|
54
|
-
updatedAt: source.updatedAt ? String(source.updatedAt).trim() : undefined
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function normalizeShardPath(input) {
|
|
59
|
-
if (input === undefined || input === null) return "";
|
|
60
|
-
const raw = String(input).trim();
|
|
61
|
-
if (!raw) return "";
|
|
62
|
-
|
|
63
|
-
const slashNormalized = raw
|
|
64
|
-
.replace(/\\/g, "/")
|
|
65
|
-
.replace(/\/+/g, "/")
|
|
66
|
-
.replace(/\/+$/, "");
|
|
67
|
-
|
|
68
|
-
if (!slashNormalized) return "";
|
|
69
|
-
if (slashNormalized.startsWith("/") || /^[A-Za-z]:\//.test(slashNormalized)) {
|
|
70
|
-
return slashNormalized;
|
|
71
|
-
}
|
|
72
|
-
return normalizeRepoPath(slashNormalized);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function normalizeReposMap(repos) {
|
|
76
|
-
const input = repos && typeof repos === "object" ? repos : {};
|
|
77
|
-
const normalized = {};
|
|
78
|
-
const sourceByKey = new Map();
|
|
79
|
-
|
|
80
|
-
for (const [repoKey, entry] of Object.entries(input)) {
|
|
81
|
-
const normalizedEntry = normalizeRepoEntry(repoKey, entry);
|
|
82
|
-
const key = normalizeRepoKey(normalizedEntry.path || repoKey);
|
|
83
|
-
if (!key) continue;
|
|
84
|
-
|
|
85
|
-
const existingEntry = normalized[key];
|
|
86
|
-
if (existingEntry && existingEntry.path !== normalizedEntry.path) {
|
|
87
|
-
const existingSource = sourceByKey.get(key) || existingEntry.path;
|
|
88
|
-
throw new Error(
|
|
89
|
-
`Repo key collision for '${key}': '${existingSource}' and '${repoKey}' normalize to different paths ('${existingEntry.path}' vs '${normalizedEntry.path}')`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
sourceByKey.set(key, repoKey);
|
|
94
|
-
normalized[key] = normalizedEntry;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return Object.fromEntries(Object.entries(normalized).sort(([a], [b]) => a.localeCompare(b)));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function computeRepoSignature({
|
|
101
|
-
repo,
|
|
102
|
-
embeddingModel,
|
|
103
|
-
indexConfig,
|
|
104
|
-
indexVersion,
|
|
105
|
-
schemaVersion = SHARED_STATE_SCHEMA_VERSION
|
|
106
|
-
}) {
|
|
107
|
-
const normalizedRepoPath = normalizeRepoPath(repo?.path);
|
|
108
|
-
const commit = String(repo?.commit || "").trim();
|
|
109
|
-
const model = String(embeddingModel || "").trim();
|
|
110
|
-
const marker = String(indexVersion || "").trim();
|
|
111
|
-
|
|
112
|
-
if (!normalizedRepoPath) throw new Error("repo.path is required to compute repo signature");
|
|
113
|
-
if (!commit) throw new Error("repo.commit is required to compute repo signature");
|
|
114
|
-
if (!model) throw new Error("embeddingModel is required to compute repo signature");
|
|
115
|
-
if (!marker) throw new Error("indexVersion is required to compute repo signature");
|
|
116
|
-
|
|
117
|
-
const payload = {
|
|
118
|
-
schemaVersion,
|
|
119
|
-
indexVersion: marker,
|
|
120
|
-
embeddingModel: model,
|
|
121
|
-
repo: {
|
|
122
|
-
path: normalizedRepoPath,
|
|
123
|
-
commit
|
|
124
|
-
},
|
|
125
|
-
indexConfig: stableValue(indexConfig && typeof indexConfig === "object" ? indexConfig : {})
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
return createHash("sha256").update(stableStringify(payload)).digest("hex");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function createSharedState({
|
|
132
|
-
generatedAt = new Date().toISOString(),
|
|
133
|
-
indexVersion,
|
|
134
|
-
repos = {},
|
|
135
|
-
schemaVersion = SHARED_STATE_SCHEMA_VERSION
|
|
136
|
-
} = {}) {
|
|
137
|
-
return {
|
|
138
|
-
schemaVersion,
|
|
139
|
-
generatedAt: String(generatedAt || "").trim(),
|
|
140
|
-
indexVersion: String(indexVersion || "").trim(),
|
|
141
|
-
repos: normalizeReposMap(repos)
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function validateSharedState(state) {
|
|
146
|
-
const errors = [];
|
|
147
|
-
|
|
148
|
-
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
149
|
-
return { ok: false, errors: ["state must be an object"] };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (state.schemaVersion !== SHARED_STATE_SCHEMA_VERSION) {
|
|
153
|
-
errors.push(`schemaVersion must be ${SHARED_STATE_SCHEMA_VERSION}`);
|
|
154
|
-
}
|
|
155
|
-
if (!String(state.generatedAt || "").trim()) {
|
|
156
|
-
errors.push("generatedAt is required");
|
|
157
|
-
}
|
|
158
|
-
if (!String(state.indexVersion || "").trim()) {
|
|
159
|
-
errors.push("indexVersion is required");
|
|
160
|
-
}
|
|
161
|
-
if (!state.repos || typeof state.repos !== "object" || Array.isArray(state.repos)) {
|
|
162
|
-
errors.push("repos must be an object map");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (state.repos && typeof state.repos === "object" && !Array.isArray(state.repos)) {
|
|
166
|
-
for (const [key, repo] of Object.entries(state.repos)) {
|
|
167
|
-
if (!String(key || "").trim()) {
|
|
168
|
-
errors.push("repo key must be a non-empty string");
|
|
169
|
-
}
|
|
170
|
-
if (!repo || typeof repo !== "object" || Array.isArray(repo)) {
|
|
171
|
-
errors.push(`repo entry '${key}' must be an object`);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const path = normalizeRepoPath(repo.path);
|
|
176
|
-
if (!path) errors.push(`repo entry '${key}' is missing path`);
|
|
177
|
-
if (!String(repo.commit || "").trim()) errors.push(`repo entry '${key}' is missing commit`);
|
|
178
|
-
if (!String(repo.signature || "").trim()) errors.push(`repo entry '${key}' is missing signature`);
|
|
179
|
-
if (!normalizeRepoPath(repo.shardPath)) errors.push(`repo entry '${key}' is missing shardPath`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
ok: errors.length === 0,
|
|
185
|
-
errors
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function loadSharedState(input) {
|
|
190
|
-
const parsed = typeof input === "string" ? JSON.parse(input) : input;
|
|
191
|
-
const state = {
|
|
192
|
-
schemaVersion: parsed?.schemaVersion,
|
|
193
|
-
generatedAt: parsed?.generatedAt,
|
|
194
|
-
indexVersion: parsed?.indexVersion,
|
|
195
|
-
repos: normalizeReposMap(parsed?.repos)
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const validation = validateSharedState(state);
|
|
199
|
-
if (!validation.ok) {
|
|
200
|
-
throw new Error(`Invalid shared state: ${validation.errors.join("; ")}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return state;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export {
|
|
207
|
-
SHARED_STATE_SCHEMA_VERSION,
|
|
208
|
-
normalizeRepoPath,
|
|
209
|
-
normalizeRepoKey,
|
|
210
|
-
computeRepoSignature,
|
|
211
|
-
createSharedState,
|
|
212
|
-
validateSharedState,
|
|
213
|
-
loadSharedState
|
|
214
|
-
};
|