@dotdotgod/cli 0.2.14 → 0.2.17
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/README.md +8 -8
- package/package.json +1 -1
- package/src/cli/usage.mjs +0 -16
- package/src/commands/config.mjs +4 -2
- package/src/commands/graph.mjs +5 -1
- package/src/commands/query.mjs +37 -7
- package/src/commands/traceability.mjs +5 -2
- package/src/core.mjs +6 -20
- package/src/docs/traceability.mjs +40 -29
- package/src/graph/communities.mjs +1 -2
- package/src/graph/metadata.mjs +18 -14
- package/src/graph/semantic.mjs +1 -1
- package/src/impact/format.mjs +3 -1
- package/src/impact/report.mjs +27 -11
- package/src/impact/scoring.mjs +29 -41
- package/src/impact/vector-overlay.mjs +49 -0
- package/src/impact/vector-profile.mjs +87 -0
- package/src/index/cache.mjs +1 -2
- package/src/index/constants.mjs +1 -1
- package/src/memory/config.mjs +82 -128
- package/src/memory/summary.mjs +2 -96
- package/src/query/store.mjs +16 -0
- package/src/validate/run.mjs +19 -15
- package/src/trello/client.mjs +0 -93
- package/src/trello/custom-fields.mjs +0 -102
- package/src/trello/github-url.mjs +0 -107
- package/src/trello/metadata.mjs +0 -52
- package/src/trello/report.mjs +0 -60
- package/src/trello/summary.mjs +0 -60
- package/src/trello/sync.mjs +0 -205
package/src/impact/report.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { DEFAULT_IMPACT_RANKING_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, defaultMemoryConfig } from '../memory/config.mjs';
|
|
1
|
+
import { DEFAULT_IMPACT_RANKING_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, defaultMemoryConfig, traceabilityRelationWeights } from '../memory/config.mjs';
|
|
2
2
|
import { retrievalMetadataForPath } from '../graph/metadata.mjs';
|
|
3
3
|
import { buildPersonalizedPageRank, compareImpactItems, docsArea, hasCuratedImpactReason, isLowActionabilityImpactItem, isSemanticOnlyImpactItem, isTestPath, scoreImpactItem } from './scoring.mjs';
|
|
4
|
+
import { normalizeChangedPath } from './vector-profile.mjs';
|
|
5
|
+
import { MAX_VECTOR_EVIDENCE_CHUNK_ID_CHARS, MAX_VECTOR_EVIDENCE_HEADING_CHARS } from './vector-overlay.mjs';
|
|
4
6
|
|
|
5
7
|
function addImpactItem(group, item, limit = 10) {
|
|
6
8
|
if (group.items.some((existing) => existing.id === item.id)) return;
|
|
@@ -13,7 +15,7 @@ function addImpactItem(group, item, limit = 10) {
|
|
|
13
15
|
|
|
14
16
|
function normalizeChangedPaths(changedPaths) {
|
|
15
17
|
const values = Array.isArray(changedPaths) ? changedPaths : [changedPaths];
|
|
16
|
-
return [...new Set(values.
|
|
18
|
+
return [...new Set(values.map(normalizeChangedPath).filter(Boolean))];
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
function selectImpactItems(sortedItems, maxRelated, seeds) {
|
|
@@ -48,9 +50,14 @@ function selectImpactItems(sortedItems, maxRelated, seeds) {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function buildCombinedImpactReport(index, changedPaths, limits = {}) {
|
|
51
|
-
const
|
|
53
|
+
const baseGraph = index?.graph ?? { nodes: [], edges: [] };
|
|
54
|
+
const overlay = limits.overlay ?? { status: 'disabled', edges: [] };
|
|
55
|
+
const graph = { nodes: baseGraph.nodes, edges: [...baseGraph.edges, ...(overlay.edges ?? [])] };
|
|
52
56
|
const config = index?.memoryConfig ? { ...defaultMemoryConfig(), ...index.memoryConfig } : defaultMemoryConfig();
|
|
53
57
|
const policy = cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY);
|
|
58
|
+
const traceabilityWeights = traceabilityRelationWeights(config.traceability);
|
|
59
|
+
policy.relationWeights = { ...policy.relationWeights, ...traceabilityWeights };
|
|
60
|
+
const curatedRelations = new Set(Object.keys(traceabilityWeights));
|
|
54
61
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
55
62
|
const seeds = changedPaths.map((path) => `file:${path}`);
|
|
56
63
|
const seedSet = new Set(seeds);
|
|
@@ -58,6 +65,7 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
|
|
|
58
65
|
const groups = { files: { items: [], omitted: 0 }, docs: { items: [], omitted: 0 }, contracts: { items: [], omitted: 0 }, tests: { items: [], omitted: 0 }, commands: { items: [], omitted: 0 }, events: { items: [], omitted: 0 }, packageResources: { items: [], omitted: 0 }, symbols: { items: [], omitted: 0 } };
|
|
59
66
|
const relatedIds = new Set(seeds);
|
|
60
67
|
const reasons = new Map(seeds.map((seed) => [seed, new Set(['changed-file'])]));
|
|
68
|
+
const vectorEvidence = new Map();
|
|
61
69
|
const addReason = (id, reason) => {
|
|
62
70
|
relatedIds.add(id);
|
|
63
71
|
if (!reasons.has(id)) reasons.set(id, new Set());
|
|
@@ -65,25 +73,31 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
|
|
|
65
73
|
};
|
|
66
74
|
|
|
67
75
|
for (const edge of graph.edges) {
|
|
76
|
+
if (edge.relation === 'vector_similarity' && seedSet.has(edge.source)) vectorEvidence.set(edge.target, {
|
|
77
|
+
score: edge.score,
|
|
78
|
+
heading: typeof edge.heading === 'string' ? edge.heading.slice(0, MAX_VECTOR_EVIDENCE_HEADING_CHARS) : undefined,
|
|
79
|
+
chunkId: typeof edge.chunkId === 'string' ? edge.chunkId.slice(0, MAX_VECTOR_EVIDENCE_CHUNK_ID_CHARS) : undefined,
|
|
80
|
+
confidence: edge.confidence,
|
|
81
|
+
});
|
|
68
82
|
if (seedSet.has(edge.source)) addReason(edge.target, edge.relation);
|
|
69
83
|
if (seedSet.has(edge.target)) addReason(edge.source, `incoming:${edge.relation}`);
|
|
70
84
|
}
|
|
71
|
-
const expansionRelations = new Set([
|
|
85
|
+
const expansionRelations = new Set([...curatedRelations, ...SEMANTIC_RELATIONS]);
|
|
72
86
|
for (const edge of graph.edges) {
|
|
73
87
|
if (!expansionRelations.has(edge.relation)) continue;
|
|
74
88
|
if (relatedIds.has(edge.source) && !seedSet.has(edge.target)) addReason(edge.target, edge.relation);
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
const pprScores = buildPersonalizedPageRank(graph, seeds, policy);
|
|
78
|
-
const candidatePprMax = Math.max(0, ...[...relatedIds].filter((id) => !seedSet.has(id)).map((id) => pprScores.get(id) ?? 0));
|
|
79
92
|
const relatedAll = [...relatedIds].map((id) => {
|
|
80
93
|
const node = nodeById.get(id) ?? { id };
|
|
81
94
|
const path = node.path ?? id.replace(/^file:/, '').replace(/^test:/, '');
|
|
82
95
|
const reasonList = [...(reasons.get(id) ?? [])];
|
|
83
96
|
const retrieval = node.retrieval ?? retrievalMetadataForPath(path);
|
|
84
97
|
const reasonSignals = reasonList.map((reason) => `reason:${reason}`);
|
|
85
|
-
const
|
|
86
|
-
|
|
98
|
+
const hasCuratedEvidence = reasonList.some((reason) => curatedRelations.has(reason.replace(/^incoming:/, '')));
|
|
99
|
+
const scored = scoreImpactItem({ ...node, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores);
|
|
100
|
+
return { ...node, reasons: reasonList, hasCuratedEvidence, ...(vectorEvidence.has(id) ? { vectorEvidence: vectorEvidence.get(id) } : {}), retrieval: { ...retrieval, signals: [...new Set([...(retrieval.signals ?? []), ...reasonSignals])] }, ...scored };
|
|
87
101
|
}).sort(compareImpactItems(seeds));
|
|
88
102
|
const related = selectImpactItems(relatedAll, maxRelated, seeds);
|
|
89
103
|
for (const item of related) {
|
|
@@ -98,7 +112,7 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
|
|
|
98
112
|
else if (item.type === 'event') addImpactItem(groups.events, item, limits.events ?? 10);
|
|
99
113
|
else if (item.type === 'package_resource') addImpactItem(groups.packageResources, item, limits.packageResources ?? 10);
|
|
100
114
|
}
|
|
101
|
-
return { changed: changedPaths[0], changedFiles: changedPaths,
|
|
115
|
+
return { changed: changedPaths[0], changedFiles: changedPaths, semantic: { status: overlay.status ?? 'disabled', ...(limits.verboseSemantic ? { diagnostics: overlay.diagnostics } : {}) }, ranking: { method: 'weighted-personalized-pagerank+memory', configSource: index?.memoryConfig?.source ?? 'default', connectionCap: policy.connectionCap, memoryCap: policy.memoryCap, pprReference: policy.ppr.reference }, related, groups, omittedRelated: Math.max(0, relatedAll.length - related.length) };
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
export function buildImpactReport(index, changedPaths, limits = {}) {
|
|
@@ -106,7 +120,9 @@ export function buildImpactReport(index, changedPaths, limits = {}) {
|
|
|
106
120
|
const aggregate = buildCombinedImpactReport(index, normalized, limits);
|
|
107
121
|
const perSeedLimit = limits.perSeed ?? 5;
|
|
108
122
|
aggregate.perSeed = normalized.map((changed) => {
|
|
109
|
-
const
|
|
123
|
+
const seedId = `file:${changed}`;
|
|
124
|
+
const seedOverlay = limits.overlay ? { ...limits.overlay, edges: (limits.overlay.edges ?? []).filter((edge) => edge.source === seedId) } : undefined;
|
|
125
|
+
const report = buildCombinedImpactReport(index, [changed], { ...limits, overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit + 1) });
|
|
110
126
|
const related = report.related.filter((item) => item.id !== `file:${changed}`).slice(0, perSeedLimit);
|
|
111
127
|
return { changed, related, omittedRelated: Math.max(0, report.related.length - 1 - related.length) + report.omittedRelated };
|
|
112
128
|
});
|
|
@@ -121,7 +137,7 @@ function compactScoreBreakdown(scoreBreakdown = {}) {
|
|
|
121
137
|
|
|
122
138
|
export function compactImpactItem(item) {
|
|
123
139
|
const compact = { id: item.id, type: item.type, impactScore: item.impactScore, reasons: (item.reasons ?? []).slice(0, 6), scoreBreakdown: compactScoreBreakdown(item.scoreBreakdown) };
|
|
124
|
-
for (const key of ['path', 'area', 'name', 'command', 'target', 'kind', 'specifier', 'title', 'contractId', 'sections']) if (item[key] !== undefined) compact[key] = item[key];
|
|
140
|
+
for (const key of ['path', 'area', 'name', 'command', 'target', 'kind', 'specifier', 'title', 'contractId', 'sections', 'vectorEvidence']) if (item[key] !== undefined) compact[key] = item[key];
|
|
125
141
|
if (item.retrieval) compact.retrieval = { area: item.retrieval.area, role: item.retrieval.role, priority: item.retrieval.priority, freshness: item.retrieval.freshness };
|
|
126
142
|
return compact;
|
|
127
143
|
}
|
|
@@ -141,5 +157,5 @@ export function buildCompactImpactReport(impact, limits = {}) {
|
|
|
141
157
|
const groups = Object.fromEntries(groupNames.map((name) => [name, compactImpactGroup(impact.groups?.[name], groupLimit)]));
|
|
142
158
|
const perSeed = (impact.perSeed ?? []).map((entry) => ({ changed: entry.changed, related: (entry.related ?? []).slice(0, limits.perSeed ?? 5).map(compactImpactItem), omittedRelated: entry.omittedRelated ?? 0 }));
|
|
143
159
|
const top10 = (impact.related ?? []).filter((item) => !seedIds.has(item.id)).slice(0, 10);
|
|
144
|
-
return { changed: impact.changed, changedFiles, perSeed, compact: true, ranking: { method: impact.ranking?.method,
|
|
160
|
+
return { changed: impact.changed, changedFiles, perSeed, semantic: impact.semantic, compact: true, ranking: { method: impact.ranking?.method, configSource: impact.ranking?.configSource, connectionCap: impact.ranking?.connectionCap, memoryCap: impact.ranking?.memoryCap, pprReference: impact.ranking?.pprReference }, related, groups, omittedRelated: (impact.omittedRelated ?? 0) + Math.max(0, (impact.related?.length ?? 0) - related.length), quality: { rawRelated: impact.related?.length ?? 0, compactRelated: related.length, semanticOnlyTop10: top10.filter((item) => isSemanticOnlyImpactItem(item)).length, curatedTop10: top10.filter((item) => hasCuratedImpactReason(item)).length, lowActionabilityTop10: top10.filter((item) => isLowActionabilityImpactItem(item)).length } };
|
|
145
161
|
}
|
package/src/impact/scoring.mjs
CHANGED
|
@@ -23,18 +23,11 @@ function roundScore(value) {
|
|
|
23
23
|
return Math.round(value * 10) / 10;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function cappedTraceabilityScore(reasons, boosts, cap) {
|
|
31
|
-
const values = reasons.map((reason) => boosts[reason] ?? 0).filter((value) => value > 0);
|
|
32
|
-
if (values.length === 0) return 0;
|
|
33
|
-
return clamp(Math.max(...values) + Math.min(5, Math.max(0, values.length - 1) * 2), 0, cap);
|
|
26
|
+
function roundProbability(value) {
|
|
27
|
+
return Math.round(value * 1000000) / 1000000;
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
export function buildPersonalizedPageRank(graph, seeds, policy) {
|
|
37
|
-
if (policy.ppr.enabled === false) return new Map();
|
|
38
31
|
const seedList = Array.isArray(seeds) ? [...new Set(seeds)] : [seeds];
|
|
39
32
|
const seedSet = new Set(seedList);
|
|
40
33
|
const restart = seedList.length > 0 ? 1 / seedList.length : 0;
|
|
@@ -45,7 +38,7 @@ export function buildPersonalizedPageRank(graph, seeds, policy) {
|
|
|
45
38
|
for (const seed of seedList) ids.add(seed);
|
|
46
39
|
const adjacency = new Map([...ids].map((id) => [id, []]));
|
|
47
40
|
for (const edge of graph.edges) {
|
|
48
|
-
const weight = policy.relationWeights[edge.relation] ?? relationWeight(edge.relation);
|
|
41
|
+
const weight = edge.weight ?? policy.relationWeights[edge.relation] ?? relationWeight(edge.relation);
|
|
49
42
|
if (weight <= 0) continue;
|
|
50
43
|
if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
|
|
51
44
|
if (!adjacency.has(edge.target)) adjacency.set(edge.target, []);
|
|
@@ -68,29 +61,33 @@ export function buildPersonalizedPageRank(graph, seeds, policy) {
|
|
|
68
61
|
return ranks;
|
|
69
62
|
}
|
|
70
63
|
|
|
71
|
-
export function scoreImpactItem(item, seeds, changedPaths, policy, pprScores
|
|
64
|
+
export function scoreImpactItem(item, seeds, changedPaths, policy, pprScores) {
|
|
72
65
|
const seedSet = seeds instanceof Set ? seeds : new Set(Array.isArray(seeds) ? seeds : [seeds]);
|
|
73
66
|
const paths = Array.isArray(changedPaths) ? changedPaths : [changedPaths];
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
const archiveSeeded = paths.some((path) => path.startsWith('docs/archive/'));
|
|
68
|
+
if (seedSet.has(item.id)) return { impactScore: 100, scoreBreakdown: { seed: 100, connection: { ppr: 80, reference: policy.ppr.reference }, memory: { priority: 20, policyAdjustments: 0 } } };
|
|
76
69
|
const retrieval = item.retrieval ?? {};
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
70
|
+
const probability = pprScores.get(item.id) ?? 0;
|
|
71
|
+
const connection = clamp((probability / policy.ppr.reference) * policy.connectionCap, 0, policy.connectionCap);
|
|
72
|
+
const priority = clamp(((retrieval.priority ?? 30) / 100) * 15, 0, 15);
|
|
73
|
+
let policyAdjustments = retrieval.freshness === 'fresh' ? 5 : retrieval.freshness === 'stale' ? -5 : 0;
|
|
74
|
+
if (retrieval.includeBodiesByDefault === false && !archiveSeeded) policyAdjustments -= 5;
|
|
75
|
+
const memory = clamp(priority + policyAdjustments, 0, policy.memoryCap);
|
|
76
|
+
const reasons = item.reasons ?? [];
|
|
77
|
+
const strongestReason = (values) => [...values].sort((a, b) => {
|
|
78
|
+
const weightDelta = (policy.relationWeights[baseImpactReason(b)] ?? relationWeight(baseImpactReason(b))) - (policy.relationWeights[baseImpactReason(a)] ?? relationWeight(baseImpactReason(a)));
|
|
79
|
+
return weightDelta || a.localeCompare(b);
|
|
80
|
+
})[0];
|
|
81
|
+
const strongestDirectRelation = strongestReason(reasons.filter((reason) => reason !== 'changed-file'));
|
|
82
|
+
const impactScore = clamp(connection + memory, 0, 100);
|
|
83
|
+
return {
|
|
84
|
+
impactScore: roundScore(impactScore),
|
|
85
|
+
scoreBreakdown: {
|
|
86
|
+
connection: { ppr: roundScore(connection), probability: roundProbability(probability), reference: policy.ppr.reference },
|
|
87
|
+
memory: { priority: roundScore(priority), policyAdjustments: roundScore(policyAdjustments) },
|
|
88
|
+
...(strongestDirectRelation ? { strongestDirectRelation } : {}),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
94
91
|
}
|
|
95
92
|
|
|
96
93
|
const CURATED_IMPACT_REASONS = new Set(['implemented_by', 'verified_by', 'related_doc', 'verification_command', 'links_to', 'routes_to', 'belongs_to_area']);
|
|
@@ -109,7 +106,7 @@ function isCuratedImpactReason(reason = '') {
|
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
export function hasCuratedImpactReason(item) {
|
|
112
|
-
return (item.reasons ?? []).some((reason) => isCuratedImpactReason(reason));
|
|
109
|
+
return item.hasCuratedEvidence === true || (item.reasons ?? []).some((reason) => isCuratedImpactReason(reason));
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
export function isSemanticOnlyImpactItem(item) {
|
|
@@ -121,15 +118,6 @@ export function isLowActionabilityImpactItem(item) {
|
|
|
121
118
|
return LOW_ACTIONABILITY_IMPACT_TYPES.has(item.type);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
|
-
function impactSelectionScore(item) {
|
|
125
|
-
let score = item.impactScore ?? 0;
|
|
126
|
-
if (hasCuratedImpactReason(item)) score += 4;
|
|
127
|
-
if (item.type === 'file' || item.type === 'test' || item.type === 'verification_command') score += 2;
|
|
128
|
-
if (isSemanticOnlyImpactItem(item)) score -= 12;
|
|
129
|
-
if (isLowActionabilityImpactItem(item)) score -= 15;
|
|
130
|
-
return score;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
121
|
export function compareImpactItems(seeds) {
|
|
134
122
|
const seedList = Array.isArray(seeds) ? seeds : [seeds];
|
|
135
123
|
const seedOrder = new Map(seedList.map((seed, index) => [seed, index]));
|
|
@@ -141,6 +129,6 @@ export function compareImpactItems(seeds) {
|
|
|
141
129
|
if (bSeed === undefined) return -1;
|
|
142
130
|
return aSeed - bSeed;
|
|
143
131
|
}
|
|
144
|
-
return
|
|
132
|
+
return (b.impactScore ?? 0) - (a.impactScore ?? 0) || a.id.localeCompare(b.id);
|
|
145
133
|
};
|
|
146
134
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildVectorIndex, rankVectorFiles } from '../commands/query.mjs';
|
|
2
|
+
import { embedTexts } from '../query/embedder.mjs';
|
|
3
|
+
import { VECTOR_MODEL } from '../query/store.mjs';
|
|
4
|
+
import { buildChangedFileProfile, canonicalizeChangedPath } from './vector-profile.mjs';
|
|
5
|
+
|
|
6
|
+
export const VECTOR_RELATION_WEIGHT = 2;
|
|
7
|
+
export const MAX_VECTOR_EVIDENCE_HEADING_CHARS = 160;
|
|
8
|
+
export const MAX_VECTOR_EVIDENCE_CHUNK_ID_CHARS = 200;
|
|
9
|
+
|
|
10
|
+
function boundedEvidence(value, limit) {
|
|
11
|
+
return typeof value === 'string' ? value.slice(0, limit) : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function buildVectorImpactOverlay(root, index, changedPaths, options = {}) {
|
|
15
|
+
const semantic = index?.memoryConfig?.impactRanking?.semantic ?? {};
|
|
16
|
+
if (semantic.enabled === false || semantic.topKPerFile === 0) return { status: 'disabled', edges: [], diagnostics: { enabled: false } };
|
|
17
|
+
const embed = options.embed ?? embedTexts;
|
|
18
|
+
const threshold = semantic.threshold ?? 0.5;
|
|
19
|
+
const topK = semantic.topKPerFile ?? 5;
|
|
20
|
+
try {
|
|
21
|
+
const vectorIndex = await (options.buildIndex ?? buildVectorIndex)(root, embed);
|
|
22
|
+
const edges = [];
|
|
23
|
+
const normalizedChangedPaths = [...new Set(changedPaths.map((path) => canonicalizeChangedPath(root, path)).filter(Boolean))];
|
|
24
|
+
for (const changed of normalizedChangedPaths) {
|
|
25
|
+
const profile = buildChangedFileProfile(root, changed, index?.graph);
|
|
26
|
+
if (!profile) continue;
|
|
27
|
+
const [queryVector] = await embed([`query: ${profile.text}`]);
|
|
28
|
+
if (!queryVector) throw new Error('Changed-file embedding was empty.');
|
|
29
|
+
const matches = rankVectorFiles(queryVector, vectorIndex)
|
|
30
|
+
.filter((match) => match.path !== changed && match.vectorScore >= threshold)
|
|
31
|
+
.slice(0, topK);
|
|
32
|
+
for (const match of matches) {
|
|
33
|
+
edges.push({
|
|
34
|
+
source: `file:${changed}`,
|
|
35
|
+
target: `file:${match.path}`,
|
|
36
|
+
relation: 'vector_similarity',
|
|
37
|
+
weight: VECTOR_RELATION_WEIGHT * match.vectorScore,
|
|
38
|
+
score: Number(match.vectorScore.toFixed(6)),
|
|
39
|
+
confidence: 'INFERRED_VECTOR_SEMANTIC',
|
|
40
|
+
heading: boundedEvidence(match.heading, MAX_VECTOR_EVIDENCE_HEADING_CHARS),
|
|
41
|
+
chunkId: boundedEvidence(match.id, MAX_VECTOR_EVIDENCE_CHUNK_ID_CHARS),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { status: 'available', edges, diagnostics: { enabled: true, model: VECTOR_MODEL, corpusChunks: vectorIndex.chunks.length, threshold, topK, embedded: vectorIndex.manifest?.embedded, reused: vectorIndex.manifest?.reused } };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return { status: 'unavailable', edges: [], diagnostics: { enabled: true, reason: error instanceof Error ? error.message : String(error) } };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { extname, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { isSecretLikePathPattern } from '../memory/config.mjs';
|
|
4
|
+
|
|
5
|
+
export const MAX_VECTOR_PROFILE_FILE_BYTES = 64 * 1024;
|
|
6
|
+
export const MAX_VECTOR_PROFILE_CHARS = 4000;
|
|
7
|
+
export const MAX_VECTOR_PROFILE_METADATA_ITEMS = 20;
|
|
8
|
+
|
|
9
|
+
const TEXT_EXTENSIONS = new Set(['.c', '.cc', '.cpp', '.css', '.go', '.h', '.hpp', '.html', '.java', '.js', '.json', '.jsx', '.md', '.mjs', '.py', '.rb', '.rs', '.sh', '.ts', '.tsx', '.txt', '.yaml', '.yml']);
|
|
10
|
+
const GENERATED_SEGMENTS = new Set(['.dotdotgod', 'build', 'coverage', 'dist', 'node_modules']);
|
|
11
|
+
|
|
12
|
+
function insideRoot(root, absolute) {
|
|
13
|
+
const normalizedRoot = resolve(root);
|
|
14
|
+
return absolute === normalizedRoot || absolute.startsWith(`${normalizedRoot}${sep}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeChangedPath(path) {
|
|
18
|
+
const normalized = String(path ?? '').replaceAll('\\', '/').replace(/^\.\/+/, '');
|
|
19
|
+
if (!normalized || normalized === '.') return null;
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function canonicalizeChangedPath(root, path) {
|
|
24
|
+
const normalized = normalizeChangedPath(path);
|
|
25
|
+
if (!normalized) return null;
|
|
26
|
+
const rootAbsolute = resolve(root);
|
|
27
|
+
const absolute = resolve(rootAbsolute, normalized);
|
|
28
|
+
if (!insideRoot(rootAbsolute, absolute)) return normalized;
|
|
29
|
+
try {
|
|
30
|
+
const canonicalRoot = realpathSync(rootAbsolute);
|
|
31
|
+
const canonicalAbsolute = realpathSync(absolute);
|
|
32
|
+
if (!insideRoot(canonicalRoot, canonicalAbsolute)) return normalized;
|
|
33
|
+
const canonical = relative(canonicalRoot, canonicalAbsolute).replaceAll('\\', '/');
|
|
34
|
+
return canonical || normalized;
|
|
35
|
+
} catch {
|
|
36
|
+
return relative(rootAbsolute, absolute).replaceAll('\\', '/') || normalized;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unsafeProfilePath(path) {
|
|
41
|
+
return isSecretLikePathPattern(path) || path.split('/').some((segment) => GENERATED_SEGMENTS.has(segment));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function graphMetadata(graph, seedId) {
|
|
45
|
+
const nodeById = new Map((graph.nodes ?? []).map((node) => [node.id, node]));
|
|
46
|
+
const values = [];
|
|
47
|
+
for (const edge of graph.edges ?? []) {
|
|
48
|
+
if (edge.source !== seedId) continue;
|
|
49
|
+
if (!['contains_heading', 'declares_package', 'declares_bin', 'includes_resource', 'depends_on'].includes(edge.relation)) continue;
|
|
50
|
+
const target = nodeById.get(edge.target);
|
|
51
|
+
const value = target?.title ?? target?.name ?? target?.target ?? target?.id;
|
|
52
|
+
if (value) values.push(`${edge.relation}: ${value}`);
|
|
53
|
+
if (values.length === MAX_VECTOR_PROFILE_METADATA_ITEMS) break;
|
|
54
|
+
}
|
|
55
|
+
return values;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function safeTextPrefix(root, path) {
|
|
59
|
+
if (unsafeProfilePath(path) || !TEXT_EXTENSIONS.has(extname(path).toLowerCase())) return '';
|
|
60
|
+
try {
|
|
61
|
+
const absolute = resolve(root, path);
|
|
62
|
+
if (!insideRoot(root, absolute)) return '';
|
|
63
|
+
const canonicalAbsolute = realpathSync(absolute);
|
|
64
|
+
if (!insideRoot(realpathSync(root), canonicalAbsolute)) return '';
|
|
65
|
+
const canonicalPath = relative(realpathSync(root), canonicalAbsolute).replaceAll('\\', '/');
|
|
66
|
+
if (canonicalPath !== path || unsafeProfilePath(canonicalPath) || !TEXT_EXTENSIONS.has(extname(canonicalPath).toLowerCase())) return '';
|
|
67
|
+
const stats = lstatSync(canonicalAbsolute);
|
|
68
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_VECTOR_PROFILE_FILE_BYTES) return '';
|
|
69
|
+
const buffer = readFileSync(canonicalAbsolute);
|
|
70
|
+
if (buffer.includes(0)) return '';
|
|
71
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
72
|
+
return text.slice(0, MAX_VECTOR_PROFILE_CHARS);
|
|
73
|
+
} catch {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildChangedFileProfile(root, path, graph = { nodes: [], edges: [] }) {
|
|
79
|
+
const normalized = canonicalizeChangedPath(root, path);
|
|
80
|
+
if (!normalized || normalized === '..' || normalized.startsWith('../') || normalized.includes('/../') || unsafeProfilePath(normalized)) return null;
|
|
81
|
+
const metadata = graphMetadata(graph, `file:${normalized}`);
|
|
82
|
+
const text = safeTextPrefix(root, normalized);
|
|
83
|
+
const sections = [`Path: ${normalized}`];
|
|
84
|
+
if (metadata.length) sections.push(`Graph metadata:\n${metadata.join('\n')}`);
|
|
85
|
+
if (text) sections.push(`Text:\n${text}`);
|
|
86
|
+
return { path: normalized, text: sections.join('\n\n').slice(0, MAX_VECTOR_PROFILE_CHARS), metadataItems: metadata.length, sourceTextIncluded: Boolean(text) };
|
|
87
|
+
}
|
package/src/index/cache.mjs
CHANGED
|
@@ -7,7 +7,6 @@ import { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shar
|
|
|
7
7
|
import { cacheFile, collectIndexFiles, fingerprint } from './files.mjs';
|
|
8
8
|
import { CACHE_VERSION } from './constants.mjs';
|
|
9
9
|
import { buildGraph } from '../graph/extract.mjs';
|
|
10
|
-
import { addDeterministicSemanticEdges } from '../graph/semantic.mjs';
|
|
11
10
|
|
|
12
11
|
function collectFingerprints(root, config = readMemoryConfig(root)) {
|
|
13
12
|
return collectIndexFiles(root, config).map((file) => {
|
|
@@ -45,7 +44,7 @@ export function buildIndex(root, previous = readIndex(root)) {
|
|
|
45
44
|
const fullRebuild = !previous?.graph || previous.version !== CACHE_VERSION;
|
|
46
45
|
const refreshReason = !previous ? 'missing' : previous.version !== CACHE_VERSION ? 'schema-mismatch' : removedPaths.length > 0 ? 'content-removed' : changedPaths.length > 0 ? 'content-changed' : 'fresh';
|
|
47
46
|
const rawGraph = fullRebuild ? buildGraph(root, files.map((file) => join(root, file.path)), memoryConfig) : mergeIncrementalGraph(previous.graph, buildGraph(root, changedFiles, memoryConfig), changedPaths);
|
|
48
|
-
const graph =
|
|
47
|
+
const graph = rawGraph;
|
|
49
48
|
const archiveBodiesIncluded = (memoryConfig.areas ?? []).some((area) => area.id === 'archive-body' && area.includeBodiesByDefault !== false);
|
|
50
49
|
return { version: CACHE_VERSION, schemaVersion: CACHE_VERSION, generatedAt: new Date().toISOString(), archiveBodiesIncluded, memoryConfig: memoryConfigSummary(memoryConfig), files, graph, stats: graphStats(graph), incremental: { enabled: true, fullRebuild, changedFiles: changedPaths.length, refreshReason, elapsedMs: Date.now() - startedAt } };
|
|
51
50
|
}
|
package/src/index/constants.mjs
CHANGED