@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.
@@ -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.filter((value) => typeof value === 'string' && value.length > 0))];
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 graph = index?.graph ?? { nodes: [], edges: [] };
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(['implemented_by', 'verified_by', 'related_doc', 'verification_command', ...SEMANTIC_RELATIONS]);
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 scored = scoreImpactItem({ ...node, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores, candidatePprMax);
86
- return { ...node, reasons: reasonList, retrieval: { ...retrieval, signals: [...new Set([...(retrieval.signals ?? []), ...reasonSignals])] }, ...scored };
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, ranking: { method: policy.ppr.enabled === false ? 'policy-score' : 'personalized-pagerank+policy', preset: policy.preset, configSource: index?.memoryConfig?.source ?? 'default', weights: policy.weights, ppr: policy.ppr }, related, groups, omittedRelated: Math.max(0, relatedAll.length - related.length) };
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 report = buildCombinedImpactReport(index, [changed], { ...limits, related: Math.max(limits.related ?? 25, perSeedLimit + 1) });
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, preset: impact.ranking?.preset, configSource: impact.ranking?.configSource }, 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 } };
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
  }
@@ -23,18 +23,11 @@ function roundScore(value) {
23
23
  return Math.round(value * 10) / 10;
24
24
  }
25
25
 
26
- function sumReasonBoosts(reasons, boosts) {
27
- return reasons.reduce((sum, reason) => sum + (boosts[reason] ?? 0), 0);
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, maxPpr) {
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
- if (seedSet.has(item.id)) return { impactScore: 100, scoreBreakdown: { seed: 100, ppr: 40, traceability: 0, memoryPolicy: 0, verification: 0, proximity: 0, semantic: 0, freshness: 0, archivePenalty: 0 } };
75
- const reasons = item.reasons ?? [];
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 pprNormalized = maxPpr > 0 ? (pprScores.get(item.id) ?? 0) / maxPpr : 0;
78
- const ppr = clamp(pprNormalized * (policy.weights.ppr ?? 40), 0, Math.abs(policy.weights.ppr ?? 40));
79
- const traceability = cappedTraceabilityScore(reasons, policy.traceabilityBoosts, Math.abs(policy.weights.traceability ?? 30));
80
- const memoryPolicy = clamp(((retrieval.priority ?? 30) / 100) * Math.abs(policy.weights.memoryPolicy ?? 10), 0, Math.abs(policy.weights.memoryPolicy ?? 10));
81
- const verification = clamp(sumReasonBoosts(reasons, policy.verificationBoosts) + (item.type === 'test' || isTestPath(item.path ?? '') ? 10 : 0) + (item.type === 'verification_command' ? 12 : 0), 0, Math.abs(policy.weights.verification ?? 15));
82
- const proximity = clamp(sumReasonBoosts(reasons, policy.proximityBoosts), 0, Math.abs(policy.weights.proximity ?? 10));
83
- const semantic = clamp(sumReasonBoosts(reasons, policy.semanticBoosts), 0, Math.abs(policy.weights.semantic ?? 10));
84
- const freshness = retrieval.freshness === 'fresh' ? Math.abs(policy.weights.freshness ?? 5) : retrieval.freshness === 'stale' ? -Math.abs(policy.weights.freshness ?? 5) : 0;
85
- let archivePenalty = 0;
86
- if (!paths.some((path) => path.startsWith('docs/archive/'))) {
87
- if (retrieval.area === 'archive-body') archivePenalty -= Math.abs(policy.weights.archivePenalty ?? -25);
88
- if (retrieval.includeBodiesByDefault === false) archivePenalty -= 10;
89
- if (retrieval.freshness === 'stale' && retrieval.area !== 'archive-map') archivePenalty -= 5;
90
- }
91
- archivePenalty = clamp(archivePenalty, -Math.abs(policy.weights.archivePenalty ?? -25), 0);
92
- const impactScore = clamp(ppr + traceability + memoryPolicy + verification + proximity + semantic + freshness + archivePenalty, 0, 100);
93
- return { impactScore: roundScore(impactScore), scoreBreakdown: { ppr: roundScore(ppr), traceability: roundScore(traceability), memoryPolicy: roundScore(memoryPolicy), verification: roundScore(verification), proximity: roundScore(proximity), semantic: roundScore(semantic), freshness: roundScore(freshness), archivePenalty: roundScore(archivePenalty) } };
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 impactSelectionScore(b) - impactSelectionScore(a) || (b.impactScore ?? 0) - (a.impactScore ?? 0) || a.id.localeCompare(b.id);
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
+ }
@@ -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 = addDeterministicSemanticEdges(rawGraph, memoryConfig);
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
  }
@@ -1,3 +1,3 @@
1
- export const CACHE_VERSION = 11;
1
+ export const CACHE_VERSION = 13;
2
2
  export const CACHE_DIR = '.dotdotgod';
3
3
  export const MANIFEST_FILE = 'manifest.json';