@dotdotgod/cli 0.6.3 → 0.6.5
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/package.json
CHANGED
package/src/commands/config.mjs
CHANGED
|
@@ -54,7 +54,6 @@ function formatConfigOutput(payload) {
|
|
|
54
54
|
lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(', ') || 'none'}`);
|
|
55
55
|
lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(', ') || 'none'}`);
|
|
56
56
|
lines.push(`- load documentation summary exclude: ${(payload.config.load?.documentationSummary?.exclude ?? []).join(', ') || 'none'}`);
|
|
57
|
-
lines.push(`- plan mode writable paths: ${(payload.config.planMode?.writablePaths ?? []).join(', ') || 'none'}`);
|
|
58
57
|
const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
|
|
59
58
|
lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(', ') || 'none'} (add: ${(lowSignal.add ?? []).join(', ') || 'none'}; remove: ${(lowSignal.remove ?? []).join(', ') || 'none'})`);
|
|
60
59
|
if (errors.length > 0) {
|
package/src/commands/query.mjs
CHANGED
|
@@ -76,7 +76,7 @@ export async function buildVectorIndex(root, embedOrOptions = {}) {
|
|
|
76
76
|
chunks.forEach((chunk, index) => { const oldIndex = cachedOffsets.get(chunk.fingerprint); if (oldIndex === undefined || !cached) missing.push({ chunk, index }); else { const d = cached.manifest.dimensions; rows[index] = Array.from(cached.vectors.subarray(oldIndex * d, (oldIndex + 1) * d)); reused += 1; } });
|
|
77
77
|
for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
|
|
78
78
|
const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
|
|
79
|
-
const embedded = await resolved.embed(batch.map(({ chunk }) =>
|
|
79
|
+
const embedded = await resolved.embed(batch.map(({ chunk }) => chunk.passage), 'document');
|
|
80
80
|
embedded.forEach((vector, index) => { rows[batch[index].index] = vector; });
|
|
81
81
|
}
|
|
82
82
|
const dimensions = rows[0]?.length ?? cached?.manifest?.dimensions;
|
|
@@ -92,7 +92,7 @@ export async function buildVectorIndex(root, embedOrOptions = {}) {
|
|
|
92
92
|
export async function queryDocumentation(root, query, options = {}) {
|
|
93
93
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
94
94
|
const index = await buildVectorIndex(root, options);
|
|
95
|
-
const [queryVector] = await index.embedding.embed([
|
|
95
|
+
const [queryVector] = await index.embedding.embed([query], 'query');
|
|
96
96
|
const dimensions = index.manifest.dimensions;
|
|
97
97
|
assertVector(queryVector, dimensions, 'Query vector');
|
|
98
98
|
const results = rankVectorFiles(queryVector, index).map((chunk) => ({ ...chunk, score: Number((chunk.vectorScore + lexicalBoost(query, chunk)).toFixed(6)), vectorScore: Number(chunk.vectorScore.toFixed(6)) }))
|
|
@@ -111,7 +111,7 @@ export async function queryProject(root, query, options = {}) {
|
|
|
111
111
|
try {
|
|
112
112
|
assertCodeEmbeddingConsent(root, options, corpus.chunks);
|
|
113
113
|
index = await buildVectorIndex(root, { ...options, chunks: corpus.chunks });
|
|
114
|
-
const [queryVector] = await index.embedding.embed([
|
|
114
|
+
const [queryVector] = await index.embedding.embed([query], 'query');
|
|
115
115
|
vector = index.chunks.map((chunk, position) => ({ ...chunk, vectorScore: cosineScore(queryVector, index.vectors, position * index.manifest.dimensions, index.manifest.dimensions) }))
|
|
116
116
|
.sort((a, b) => b.vectorScore - a.vectorScore || a.id.localeCompare(b.id));
|
|
117
117
|
} catch (error) {
|
|
@@ -24,7 +24,7 @@ export async function buildVectorImpactOverlay(root, index, changedPaths, option
|
|
|
24
24
|
if (!profile) continue;
|
|
25
25
|
const embed = vectorIndex.embedding?.embed ?? options.embed;
|
|
26
26
|
if (!embed) throw new Error('Vector index did not provide an embedder.');
|
|
27
|
-
const [queryVector] = await embed([
|
|
27
|
+
const [queryVector] = await embed([profile.text], 'query');
|
|
28
28
|
if (!queryVector) throw new Error('Changed-file embedding was empty.');
|
|
29
29
|
const matches = rankVectorFiles(queryVector, vectorIndex)
|
|
30
30
|
.filter((match) => match.path !== changed && match.vectorScore >= threshold)
|
package/src/memory/config.mjs
CHANGED
|
@@ -37,9 +37,7 @@ export const DEFAULT_LOAD_POLICY = {
|
|
|
37
37
|
exclude: ['docs/plan', 'docs/archive'],
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
-
export const DEFAULT_PLAN_MODE_POLICY = {
|
|
41
|
-
writablePaths: ['docs/plan/**', 'docs/archive/**'],
|
|
42
|
-
};
|
|
40
|
+
export const DEFAULT_PLAN_MODE_POLICY = {};
|
|
43
41
|
export const DEFAULT_IMPACT_RANKING_POLICY = {
|
|
44
42
|
connectionCap: 80,
|
|
45
43
|
memoryCap: 20,
|
|
@@ -199,8 +197,8 @@ export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
|
199
197
|
};
|
|
200
198
|
}
|
|
201
199
|
|
|
202
|
-
export function clonePlanModePolicy(
|
|
203
|
-
return {
|
|
200
|
+
export function clonePlanModePolicy() {
|
|
201
|
+
return {};
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
export function normalizeDocumentationRoot(value = DEFAULT_DOCUMENTATION_ROOT) {
|
|
@@ -215,7 +213,7 @@ export function isValidDocumentationRoot(value) {
|
|
|
215
213
|
}
|
|
216
214
|
export function defaultMemoryConfig({ documentationRoot = DEFAULT_DOCUMENTATION_ROOT } = {}) {
|
|
217
215
|
const root = normalizeDocumentationRoot(documentationRoot);
|
|
218
|
-
return { source: 'default', documentation: { root }, areas: memoryAreasForRoot(root).map(cloneArea), traceability: cloneTraceabilityPolicy({ ...DEFAULT_TRACEABILITY_POLICY, required: [`${root}/spec/**`] }), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy({ ...DEFAULT_LOAD_POLICY, documentationSummary: { exclude: [`${root}/plan`, `${root}/archive`] } }), planMode: clonePlanModePolicy(
|
|
216
|
+
return { source: 'default', documentation: { root }, areas: memoryAreasForRoot(root).map(cloneArea), traceability: cloneTraceabilityPolicy({ ...DEFAULT_TRACEABILITY_POLICY, required: [`${root}/spec/**`] }), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy({ ...DEFAULT_LOAD_POLICY, documentationSummary: { exclude: [`${root}/plan`, `${root}/archive`] } }), planMode: clonePlanModePolicy() };
|
|
219
217
|
}
|
|
220
218
|
|
|
221
219
|
export function normalizePathPattern(value = '') {
|
|
@@ -289,12 +287,9 @@ export function isSecretLikePathPattern(value = '') {
|
|
|
289
287
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
|
|
290
288
|
}
|
|
291
289
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
? DEFAULT_PLAN_MODE_POLICY.writablePaths
|
|
296
|
-
: [...new Set((Array.isArray(raw.writablePaths) ? raw.writablePaths : []).map(normalizePathPattern))];
|
|
297
|
-
return clonePlanModePolicy({ writablePaths });
|
|
290
|
+
// Legacy planMode.writablePaths is accepted but discarded on normalization.
|
|
291
|
+
function normalizePlanModePolicy(_raw) {
|
|
292
|
+
return clonePlanModePolicy();
|
|
298
293
|
}
|
|
299
294
|
|
|
300
295
|
function normalizeLoadPolicy(raw) {
|
|
@@ -437,10 +432,6 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
437
432
|
if (planMode !== undefined) {
|
|
438
433
|
if (!planMode || typeof planMode !== 'object' || Array.isArray(planMode)) {
|
|
439
434
|
add('PLAN_MODE_CONFIG_INVALID', 'planMode', 'Expected an object.');
|
|
440
|
-
} else if (planMode.writablePaths !== undefined) {
|
|
441
|
-
const paths = planMode.writablePaths;
|
|
442
|
-
if (!Array.isArray(paths)) add('PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS', 'planMode.writablePaths', 'Expected an array of documentation path strings.');
|
|
443
|
-
else if (paths.some((value) => !isValidPathPattern(value) || typeof value !== 'string' || value.trim().startsWith('/') || (!normalizePathPattern(value).startsWith(`${normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT)}/`)) || normalizePathPattern(value).startsWith(`${normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT)}/.`) || isSecretLikePathPattern(value))) add('PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS', 'planMode.writablePaths', 'Expected safe repository-relative paths under documentation.root using exact paths or /** subtree patterns.');
|
|
444
435
|
}
|
|
445
436
|
}
|
|
446
437
|
const referenceExpansion = data.referenceExpansion;
|
package/src/query/embedder.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { embeddingProfileIdentity, resolveEmbeddingProfile } from './embedding-config.mjs';
|
|
1
|
+
import { embeddingInputFormat, embeddingProfileIdentity, resolveEmbeddingProfile } from './embedding-config.mjs';
|
|
2
2
|
import { importTransformers } from './embedding-runtime.mjs';
|
|
3
3
|
|
|
4
4
|
const localPipelines = new Map();
|
|
@@ -37,7 +37,12 @@ async function postJson(url, body, profile) {
|
|
|
37
37
|
const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...(key ? { authorization: `Bearer ${key}` } : {}) }, body: JSON.stringify(body), signal: controller.signal });
|
|
38
38
|
if (!response.ok) {
|
|
39
39
|
const category = response.status === 401 || response.status === 403 ? 'authentication' : response.status === 429 ? 'rate limit' : `HTTP ${response.status}`;
|
|
40
|
-
|
|
40
|
+
const error = new Error(`Remote embedding ${category} failure.`);
|
|
41
|
+
if (response.status === 400 || response.status === 413) {
|
|
42
|
+
const body = (await response.text()).slice(0, 8192);
|
|
43
|
+
error.tokenLimit = /(?:exceed\w*[\s\S]{0,120}tokens?|tokens?[\s\S]{0,120}(?:exceed|limit|too (?:long|many))|maximum context length)/i.test(body);
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
41
46
|
}
|
|
42
47
|
try { return await response.json(); } catch { throw new Error('Remote embedding returned malformed JSON.'); }
|
|
43
48
|
} catch (error) {
|
|
@@ -60,21 +65,36 @@ function remoteEmbedder(profile) {
|
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
function withInputFormat(embed, profile) {
|
|
69
|
+
const format = embeddingInputFormat(profile);
|
|
70
|
+
return (texts, role) => {
|
|
71
|
+
if (role !== undefined && role !== 'query' && role !== 'document') throw new Error('Embedding role must be query or document.');
|
|
72
|
+
return embed(role === undefined ? texts : texts.map((text) => format[`${role}Prefix`] + text));
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
export async function createEmbedder(profile) {
|
|
64
77
|
const embed = profile.provider === 'local' ? await localEmbedder(profile) : remoteEmbedder(profile);
|
|
65
|
-
|
|
78
|
+
async function embedBatch(texts) {
|
|
66
79
|
if (texts.length === 0) return [];
|
|
67
|
-
|
|
80
|
+
let rows;
|
|
81
|
+
try { rows = await embed(texts); }
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!error.tokenLimit || texts.length === 1) throw error;
|
|
84
|
+
const middle = Math.ceil(texts.length / 2);
|
|
85
|
+
rows = [...await embedBatch(texts.slice(0, middle)), ...await embedBatch(texts.slice(middle))];
|
|
86
|
+
}
|
|
68
87
|
if (!Array.isArray(rows) || rows.length !== texts.length) throw new Error('Embedding provider returned an unexpected vector count.');
|
|
69
88
|
const dimensions = rows[0]?.length;
|
|
70
89
|
if (!dimensions || rows.some((row) => row.length !== dimensions)) throw new Error('Embedding provider returned inconsistent vector dimensions.');
|
|
71
90
|
return rows;
|
|
72
|
-
}
|
|
91
|
+
}
|
|
92
|
+
return withInputFormat(embedBatch, profile);
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
export async function resolveEmbedder(root, options = {}) {
|
|
76
96
|
const resolved = options.profile ? { profile: options.profile, source: options.source ?? 'injected', path: null } : resolveEmbeddingProfile(root, options);
|
|
77
|
-
return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed
|
|
97
|
+
return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed ? withInputFormat(options.embed, resolved.profile) : await createEmbedder(resolved.profile) };
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
export async function embedTexts(texts) {
|
|
@@ -4,6 +4,18 @@ import { join } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_EMBEDDING_PROFILE = Object.freeze({ provider: 'local', model: 'Xenova/multilingual-e5-small' });
|
|
6
6
|
export const EMBEDDING_PROVIDERS = Object.freeze(['local', 'openai-compatible', 'ollama']);
|
|
7
|
+
const DEFAULT_INPUT_FORMAT = Object.freeze({ queryPrefix: 'query: ', documentPrefix: 'passage: ' });
|
|
8
|
+
|
|
9
|
+
export function embeddingInputFormat(profile, label = 'embedding') {
|
|
10
|
+
if (profile.inputFormat === undefined) return { ...DEFAULT_INPUT_FORMAT };
|
|
11
|
+
const format = profile.inputFormat;
|
|
12
|
+
if (!format || typeof format !== 'object' || Array.isArray(format)
|
|
13
|
+
|| Object.keys(format).some((key) => !Object.hasOwn(DEFAULT_INPUT_FORMAT, key))
|
|
14
|
+
|| Object.keys(DEFAULT_INPUT_FORMAT).some((key) => typeof format[key] !== 'string')) {
|
|
15
|
+
throw new Error(`${label}.inputFormat must contain exactly queryPrefix and documentPrefix strings.`);
|
|
16
|
+
}
|
|
17
|
+
return { queryPrefix: format.queryPrefix, documentPrefix: format.documentPrefix };
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
function readJson(path, label) {
|
|
9
21
|
if (!existsSync(path)) return null;
|
|
@@ -13,7 +25,7 @@ function readJson(path, label) {
|
|
|
13
25
|
|
|
14
26
|
export function validateEmbeddingProfile(profile, label = 'embedding') {
|
|
15
27
|
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) throw new Error(`${label} must be an object.`);
|
|
16
|
-
const allowed = new Set(['provider', 'model', 'baseUrl', 'apiKey', 'apiKeyEnv']);
|
|
28
|
+
const allowed = new Set(['provider', 'model', 'baseUrl', 'apiKey', 'apiKeyEnv', 'inputFormat']);
|
|
17
29
|
const unknown = Object.keys(profile).find((key) => !allowed.has(key));
|
|
18
30
|
if (unknown) throw new Error(`${label}.${unknown} is not supported.`);
|
|
19
31
|
if (!EMBEDDING_PROVIDERS.includes(profile.provider)) throw new Error(`${label}.provider must be local, openai-compatible, or ollama.`);
|
|
@@ -27,6 +39,7 @@ export function validateEmbeddingProfile(profile, label = 'embedding') {
|
|
|
27
39
|
if (!['http:', 'https:'].includes(url.protocol)) throw new Error(`${label}.baseUrl must use HTTP or HTTPS.`);
|
|
28
40
|
if (url.username || url.password) throw new Error(`${label}.baseUrl must not contain credentials.`);
|
|
29
41
|
}
|
|
42
|
+
embeddingInputFormat(profile, label);
|
|
30
43
|
return { ...profile, model: profile.model.trim(), ...(profile.baseUrl ? { baseUrl: profile.baseUrl.replace(/\/$/, '') } : {}) };
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -36,8 +49,11 @@ export function sanitizeEmbeddingProfile(profile) {
|
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
export function embeddingProfileIdentity(profile) {
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
return {
|
|
53
|
+
provider: profile.provider, model: profile.model,
|
|
54
|
+
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
55
|
+
inputFormatVersion: 1, inputFormat: embeddingInputFormat(profile),
|
|
56
|
+
};
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
export function resolveEmbeddingProfile(root = '.', options = {}) {
|