@dotdotgod/cli 0.2.15 → 0.2.18
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 -6
- package/package.json +1 -1
- package/src/commands/config.mjs +4 -1
- 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 +4 -2
- 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 +80 -89
- package/src/memory/summary.mjs +2 -96
- package/src/query/store.mjs +16 -0
- package/src/validate/run.mjs +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ dotdotgod graph impact . --changed <path> --compact
|
|
|
38
38
|
- `index` builds `.dotdotgod/manifest.json` and compact graph shards from maintained project files.
|
|
39
39
|
- `query` locally embeds shared Markdown with `Xenova/multilingual-e5-small`, incrementally stores vectors under `.dotdotgod/vectors/`, and returns the best-ranked chunk from each relevant Markdown file.
|
|
40
40
|
- `resolve` and `expand` map explicit or high-signal prompt references to project files.
|
|
41
|
-
- `graph impact` ranks likely related specs, tests, docs, commands, and source files for one or more changed paths, with a combined ranking and per-file top five.
|
|
41
|
+
- `graph impact` ranks likely related specs, tests, docs, commands, and source files for one or more changed paths, with a combined ranking and per-file top five. Non-seed scores use fixed weighted PPR connection `80` plus memory policy `20`. A bounded request-local multilingual vector overlay participates in PPR when the query cache is available; vector preparation failures degrade to structural-only results.
|
|
42
42
|
- `traceability links` checks or repairs generated Markdown traceability-link sections.
|
|
43
43
|
|
|
44
44
|
## Commands
|
|
@@ -85,17 +85,19 @@ $ dotdotgod graph impact . --changed packages/cli/src/core.mjs --compact
|
|
|
85
85
|
|
|
86
86
|
```text
|
|
87
87
|
docs:
|
|
88
|
-
- docs/spec/
|
|
89
|
-
- docs/
|
|
88
|
+
- docs/spec/CONFIG_COMMAND.md (56.8; incoming:links_to, incoming:implemented_by)
|
|
89
|
+
- docs/spec/cli/TRACEABILITY_LINKS.md (26.3; vector_similarity)
|
|
90
|
+
|
|
91
|
+
tests:
|
|
92
|
+
- packages/cli/test/core.test.mjs (6.5; verified_by)
|
|
90
93
|
|
|
91
94
|
files:
|
|
92
95
|
- packages/cli/src/core.mjs (100; changed-file)
|
|
93
|
-
- packages/pi/extensions/plan-mode/index.ts (45; implemented_by, semantic_similarity)
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
`graph impact` needs at least one `--changed <path>` and accepts repeated options. Multi-file output preserves input order, deduplicates repeated paths, returns a bounded combined ranking, and includes the top five non-seed results for each changed file. Use `--compact` for short text, `--yml` or `--yaml` for compact structured agent-facing output, and `--json` for machine-readable detail.
|
|
97
99
|
|
|
98
|
-
The graph is built from maintained project files: Markdown links, README routes, headings, traceability
|
|
100
|
+
The indexed graph is built from maintained project files: Markdown links, README routes, headings, configured traceability relations, package metadata, scripts and packaged resources, dependencies, and memory-area membership. Impact analysis can add request-local vector edges for candidate discovery and PPR without mutating that indexed graph.
|
|
99
101
|
|
|
100
102
|
## Validation and Verification Boundaries
|
|
101
103
|
|
|
@@ -119,7 +121,7 @@ The default scaffold gives docs explicit roles:
|
|
|
119
121
|
- `docs/archive/README.md`: local archive map included by default.
|
|
120
122
|
- `docs/archive/**`: local archive bodies excluded by default unless targeted.
|
|
121
123
|
|
|
122
|
-
Use `memory.areas` to customize memory classification and retrieval priority. Use `traceability.required` and `traceability.exclude` to
|
|
124
|
+
Use `memory.areas` to customize memory classification and retrieval priority. Use `traceability.required` and `traceability.exclude` to select which Markdown paths need `json dotdotgod` blocks, and use ordered `traceability.keys` definitions to configure string-array labels, path or command targets, graph relations, and PPR weights. Use `validation.markdown` for size budgets and `referenceExpansion` for fuzzy prompt matching. The complete `impactRanking` namespace is non-blocking compatibility input: valid semantic candidate controls apply, malformed values fall back to defaults, and retired or unknown fields are ignored without changing fixed scoring. Legacy `load.pinnedPaths` and `load.pinnedBodies` values are also ignored.
|
|
123
125
|
|
|
124
126
|
## Indexing Scope
|
|
125
127
|
|
package/package.json
CHANGED
package/src/commands/config.mjs
CHANGED
|
@@ -36,9 +36,12 @@ function formatConfigOutput(payload) {
|
|
|
36
36
|
lines.push(`- memory areas: ${payload.config.areas.length}`);
|
|
37
37
|
lines.push(`- traceability required: ${(payload.config.traceability.required ?? []).join(', ') || 'none'}`);
|
|
38
38
|
lines.push(`- traceability exclude: ${(payload.config.traceability.exclude ?? []).join(', ') || 'none'}`);
|
|
39
|
+
lines.push('- traceability keys:');
|
|
40
|
+
for (const definition of payload.config.traceability.keys ?? []) lines.push(` - ${definition.key}: ${definition.target} -> ${definition.relation} (weight=${definition.weight}; label=${definition.label})`);
|
|
39
41
|
lines.push(`- validation markdown: maxLines=${payload.config.validation.markdown.maxLines}, maxChars=${payload.config.validation.markdown.maxChars}`);
|
|
40
42
|
lines.push(`- validation markdown exclude: ${(payload.config.validation.markdown.exclude ?? []).join(', ') || 'none'}`);
|
|
41
|
-
|
|
43
|
+
const fixedImpact = payload.config.impactRanking.fixed;
|
|
44
|
+
lines.push(`- impact ranking: fixed PPR=${fixedImpact.connectionCap}, memory=${fixedImpact.memoryCap}, reference=${fixedImpact.ppr.reference}`);
|
|
42
45
|
lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(', ') || 'none'}`);
|
|
43
46
|
lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(', ') || 'none'}`);
|
|
44
47
|
lines.push(`- load documentation summary exclude: ${(payload.config.load?.documentationSummary?.exclude ?? []).join(', ') || 'none'}`);
|
package/src/commands/graph.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { commandUsage, parseCommon, usage } from '../cli/usage.mjs';
|
|
|
2
2
|
import { buildCommunities } from '../graph/communities.mjs';
|
|
3
3
|
import { graphSummary, readFreshIndex } from '../index/cache.mjs';
|
|
4
4
|
import { buildCompactImpactReport, buildImpactReport } from '../impact/report.mjs';
|
|
5
|
+
import { buildVectorImpactOverlay } from '../impact/vector-overlay.mjs';
|
|
6
|
+
import { canonicalizeChangedPath } from '../impact/vector-profile.mjs';
|
|
5
7
|
import { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from '../impact/format.mjs';
|
|
6
8
|
|
|
7
9
|
const MAX_CHANGED_FILES = 20;
|
|
@@ -65,8 +67,10 @@ export async function runGraph(argv) {
|
|
|
65
67
|
}
|
|
66
68
|
usage(message, 'graph impact');
|
|
67
69
|
}
|
|
70
|
+
if (isImpact) options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
|
|
68
71
|
const { status, index, metadata } = readFreshIndex(options.root);
|
|
69
|
-
const
|
|
72
|
+
const overlay = isImpact ? await buildVectorImpactOverlay(options.root, index, options.changed) : undefined;
|
|
73
|
+
const rawImpact = isImpact ? buildImpactReport(index, options.changed, { overlay, verboseSemantic: options.json }) : undefined;
|
|
70
74
|
const impact = isImpact && (options.compact || options.yml) ? buildCompactImpactReport(rawImpact) : rawImpact;
|
|
71
75
|
const payload = isImpact
|
|
72
76
|
? { ok: status.ok, command: 'graph impact', compact: options.compact || undefined, root: options.root, status, metadata, changed: options.changed[0], changedFiles: options.changed, related: impact.related, impact }
|
package/src/commands/query.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { usage } from '../cli/usage.mjs';
|
|
|
3
3
|
import { readMemoryConfig } from '../memory/config.mjs';
|
|
4
4
|
import { collectDocumentationChunks } from '../query/chunks.mjs';
|
|
5
5
|
import { embedTexts } from '../query/embedder.mjs';
|
|
6
|
-
import { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, writeVectorCache } from '../query/store.mjs';
|
|
6
|
+
import { isValidNormalizedVectorValues, readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, writeVectorCache } from '../query/store.mjs';
|
|
7
7
|
|
|
8
8
|
const DEFAULT_LIMIT = 30;
|
|
9
9
|
const MAX_LIMIT = 100;
|
|
@@ -31,10 +31,33 @@ export function parseQueryOptions(argv) {
|
|
|
31
31
|
return options;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function
|
|
34
|
+
function assertNormalizedVector(vector, label) {
|
|
35
|
+
if (!vector || vector.length !== VECTOR_DIMENSIONS || !isValidNormalizedVectorValues(vector)) throw new Error(`${label} must be a finite normalized ${VECTOR_DIMENSIONS}-dimensional vector.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cosineScore(left, vectors, offset = 0) {
|
|
39
|
+
assertNormalizedVector(left, 'Query vector');
|
|
40
|
+
const right = vectors?.subarray ? vectors.subarray(offset, offset + VECTOR_DIMENSIONS) : vectors?.slice(offset, offset + VECTOR_DIMENSIONS);
|
|
41
|
+
assertNormalizedVector(right, 'Passage vector');
|
|
35
42
|
let score = 0;
|
|
36
|
-
for (let index = 0; index < VECTOR_DIMENSIONS; index += 1) score += left[index] *
|
|
37
|
-
|
|
43
|
+
for (let index = 0; index < VECTOR_DIMENSIONS; index += 1) score += left[index] * right[index];
|
|
44
|
+
if (!Number.isFinite(score) || score < -1.000001 || score > 1.000001) throw new Error('Cosine similarity was outside the finite normalized range.');
|
|
45
|
+
return Math.max(-1, Math.min(1, score));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function rankVectorFiles(queryVector, index) {
|
|
49
|
+
assertNormalizedVector(queryVector, 'Query vector');
|
|
50
|
+
if (!index || !Array.isArray(index.chunks) || index.vectors?.length !== index.chunks.length * VECTOR_DIMENSIONS || !isValidNormalizedVectorValues(index.vectors)) throw new Error('Vector index contains invalid passage vectors.');
|
|
51
|
+
const ranked = index.chunks.map((chunk, position) => ({ ...chunk, vectorScore: cosineScore(queryVector, index.vectors, position * VECTOR_DIMENSIONS) }))
|
|
52
|
+
.sort((left, right) => right.vectorScore - left.vectorScore || left.path.localeCompare(right.path) || left.id.localeCompare(right.id));
|
|
53
|
+
const results = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const chunk of ranked) {
|
|
56
|
+
if (seen.has(chunk.path)) continue;
|
|
57
|
+
seen.add(chunk.path);
|
|
58
|
+
results.push(chunk);
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
function lexicalBoost(query, chunk) {
|
|
@@ -66,7 +89,11 @@ export async function buildVectorIndex(root, embed = embedTexts) {
|
|
|
66
89
|
for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
|
|
67
90
|
const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
|
|
68
91
|
const embedded = await embed(batch.map(({ chunk }) => `passage: ${chunk.passage}`));
|
|
69
|
-
|
|
92
|
+
if (!Array.isArray(embedded) || embedded.length !== batch.length) throw new Error('Embedder returned an unexpected passage-vector count.');
|
|
93
|
+
embedded.forEach((vector, index) => {
|
|
94
|
+
assertNormalizedVector(vector, 'Passage vector');
|
|
95
|
+
vectors.set(vector, batch[index].index * VECTOR_DIMENSIONS);
|
|
96
|
+
});
|
|
70
97
|
}
|
|
71
98
|
const storedChunks = chunks.map(({ passage, ...chunk }) => chunk);
|
|
72
99
|
const manifest = writeVectorCache(root, storedChunks, vectors, {
|
|
@@ -81,9 +108,12 @@ export async function queryDocumentation(root, query, options = {}) {
|
|
|
81
108
|
const embed = options.embed ?? embedTexts;
|
|
82
109
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
83
110
|
const index = await buildVectorIndex(root, embed);
|
|
84
|
-
const
|
|
111
|
+
const embeddedQuery = await embed([`query: ${query}`]);
|
|
112
|
+
if (!Array.isArray(embeddedQuery) || embeddedQuery.length !== 1) throw new Error('Embedder returned an unexpected query-vector count.');
|
|
113
|
+
const [queryVector] = embeddedQuery;
|
|
114
|
+
assertNormalizedVector(queryVector, 'Query vector');
|
|
85
115
|
const rankedChunks = index.chunks.map((chunk, position) => {
|
|
86
|
-
const vectorScore =
|
|
116
|
+
const vectorScore = cosineScore(queryVector, index.vectors, position * VECTOR_DIMENSIONS);
|
|
87
117
|
return { ...chunk, score: Number((vectorScore + lexicalBoost(query, chunk)).toFixed(6)), vectorScore: Number(vectorScore.toFixed(6)) };
|
|
88
118
|
}).sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
89
119
|
const results = [];
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { usage } from '../cli/usage.mjs';
|
|
4
4
|
import { rel } from '../common/paths.mjs';
|
|
5
5
|
import { extractDotdotgodTraceabilityBlocks, syncTraceabilityLinksInContent, validateTraceabilityLinksRegion } from '../docs/traceability.mjs';
|
|
6
|
+
import { readMemoryConfig } from '../memory/config.mjs';
|
|
6
7
|
|
|
7
8
|
function collectDocsMarkdownFiles(root) {
|
|
8
9
|
const docs = join(root, 'docs');
|
|
@@ -43,15 +44,17 @@ export function parseTraceabilityOptions(argv) {
|
|
|
43
44
|
|
|
44
45
|
export function runTraceability(argv) {
|
|
45
46
|
const options = parseTraceabilityOptions(argv);
|
|
47
|
+
const config = readMemoryConfig(options.root);
|
|
46
48
|
const files = collectDocsMarkdownFiles(options.root);
|
|
47
49
|
const results = [];
|
|
48
|
-
const errors = [];
|
|
50
|
+
const errors = [...(config.errors ?? [])];
|
|
49
51
|
for (const file of files) {
|
|
52
|
+
if (options.write && errors.length > 0) break;
|
|
50
53
|
const content = readFileSync(file, 'utf8');
|
|
51
54
|
for (const error of validateTraceabilityLinksRegion(content, options.root, file)) errors.push(error);
|
|
52
55
|
const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
|
|
53
56
|
if (blocks.length === 0) continue;
|
|
54
|
-
const result = syncTraceabilityLinksInContent(content, blocks.at(-1).data, options.root, file);
|
|
57
|
+
const result = syncTraceabilityLinksInContent(content, blocks.at(-1).data, options.root, file, config);
|
|
55
58
|
if (!result.ok) {
|
|
56
59
|
for (const error of result.errors ?? []) errors.push(error);
|
|
57
60
|
continue;
|
package/src/core.mjs
CHANGED
|
@@ -17,16 +17,18 @@ export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_
|
|
|
17
17
|
export { buildCommunities, relationWeight } from './graph/communities.mjs';
|
|
18
18
|
export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
|
|
19
19
|
export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
|
|
20
|
-
export { buildMemoryAreas,
|
|
20
|
+
export { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from './memory/summary.mjs';
|
|
21
21
|
export { isReadmeIndexPath, retrievalMetadataForPath } from './graph/metadata.mjs';
|
|
22
22
|
export { buildGraph } from './graph/extract.mjs';
|
|
23
23
|
export { addDeterministicSemanticEdges } from './graph/semantic.mjs';
|
|
24
24
|
export { buildIndex, writeIndex, readIndex, getStatus, runIndex, runStatus, readFreshIndex, graphSummary } from './index/cache.mjs';
|
|
25
25
|
export { runValidate } from './validate/run.mjs';
|
|
26
26
|
export { buildImpactReport, buildCompactImpactReport } from './impact/report.mjs';
|
|
27
|
+
export { buildChangedFileProfile, MAX_VECTOR_PROFILE_CHARS, MAX_VECTOR_PROFILE_FILE_BYTES, MAX_VECTOR_PROFILE_METADATA_ITEMS } from './impact/vector-profile.mjs';
|
|
28
|
+
export { buildVectorImpactOverlay, VECTOR_RELATION_WEIGHT } from './impact/vector-overlay.mjs';
|
|
27
29
|
export { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from './impact/format.mjs';
|
|
28
30
|
export { runConfig } from './commands/config.mjs';
|
|
29
|
-
export { buildVectorIndex, parseQueryOptions, queryDocumentation, runQuery } from './commands/query.mjs';
|
|
31
|
+
export { buildVectorIndex, cosineScore, parseQueryOptions, queryDocumentation, rankVectorFiles, runQuery } from './commands/query.mjs';
|
|
30
32
|
export { chunkMarkdown, collectDocumentationChunks, collectDocumentationMarkdown, textFingerprint } from './query/chunks.mjs';
|
|
31
33
|
export { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, VECTOR_SCHEMA_VERSION, vectorCachePaths, writeVectorCache } from './query/store.mjs';
|
|
32
34
|
export { extractBracketReferences, extractFuzzyReferences, normalizeReferenceAlias } from './reference/extract.mjs';
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, extname, relative, resolve } from 'node:path';
|
|
3
3
|
import { rel } from '../common/paths.mjs';
|
|
4
|
-
import { defaultMemoryConfig, resolveMemoryArea } from '../memory/config.mjs';
|
|
4
|
+
import { DEFAULT_TRACEABILITY_KEYS, defaultMemoryConfig, resolveMemoryArea } from '../memory/config.mjs';
|
|
5
5
|
|
|
6
6
|
function isSecretIndexPath(path) {
|
|
7
7
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials|secrets?)(\.|\/|$)/i.test(path);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
function traceabilityKeys(config = defaultMemoryConfig()) {
|
|
11
|
+
return config.traceability?.keys ?? DEFAULT_TRACEABILITY_KEYS;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function traceabilityDefinitionMap(config = defaultMemoryConfig()) {
|
|
15
|
+
return new Map(traceabilityKeys(config).map((definition) => [definition.key, definition]));
|
|
16
|
+
}
|
|
13
17
|
export const TRACEABILITY_LINKS_START = '<!-- dotdotgod:traceability-links:start version=1 source=json-dotdotgod -->';
|
|
14
18
|
export const TRACEABILITY_LINKS_END = '<!-- dotdotgod:traceability-links:end -->';
|
|
15
19
|
|
|
16
|
-
export function traceabilityExample() {
|
|
17
|
-
|
|
20
|
+
export function traceabilityExample(config = defaultMemoryConfig()) {
|
|
21
|
+
const fields = traceabilityKeys(config).map((definition) => ` "${definition.key}": [${definition.target === 'command' ? '"pnpm ..."' : '"packages/..."'}]`).join(',\n');
|
|
22
|
+
const guidance = traceabilityKeys(config).map((definition) => `- ${definition.key}: ${definition.description}`).join('\n');
|
|
23
|
+
return `Expected dotdotgod traceability block:\n\n\`\`\`json dotdotgod\n{\n "kind": "spec"${fields ? `,\n${fields}` : ''}\n}\n\`\`\`\n\nProperty guidance:\n- kind: use "spec" for behavior specs.\n${guidance}${guidance ? '\n' : ''}- contracts: optional focused behavior contracts; each contract requires non-empty id and title, may include sections, and may include configured traceability fields. Unknown contract fields are validation errors.`;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
function lineForOffset(content, offset) {
|
|
@@ -46,8 +52,8 @@ export function isLocalRelativeTraceabilityPath(value) {
|
|
|
46
52
|
return !isSecretIndexPath(value);
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
function traceabilityFieldError(file, code, field, message, line = null) {
|
|
50
|
-
return { file, line, code, message: `${field ? `Field "${field}": ` : ''}${message}\nFix: update the traceability block so it matches the expected schema and points to existing project files or commands.\n\n${traceabilityExample()}` };
|
|
55
|
+
function traceabilityFieldError(file, code, field, message, line = null, config = defaultMemoryConfig()) {
|
|
56
|
+
return { file, line, code, message: `${field ? `Field "${field}": ` : ''}${message}\nFix: update the traceability block so it matches the expected schema and points to existing project files or commands.\n\n${traceabilityExample(config)}` };
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export function validateTraceabilityPlacement(content, root, file) {
|
|
@@ -124,12 +130,13 @@ function renderPathList(title, paths, root, file) {
|
|
|
124
130
|
return lines;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
function
|
|
128
|
-
if (!Array.isArray(
|
|
129
|
-
return [
|
|
133
|
+
function renderDefinitionList(definition, values, root, file) {
|
|
134
|
+
if (!Array.isArray(values) || values.length === 0) return [];
|
|
135
|
+
if (definition.target === 'command') return [`- ${definition.label}:`, ...values.map((command) => ` - \`${escapeCodeSpan(command)}\``)];
|
|
136
|
+
return renderPathList(definition.label, values, root, file);
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
function renderContractList(contracts) {
|
|
139
|
+
function renderContractList(contracts, definitions) {
|
|
133
140
|
if (!Array.isArray(contracts) || contracts.length === 0) return [];
|
|
134
141
|
const lines = ['- Contracts:'];
|
|
135
142
|
for (const contract of contracts) {
|
|
@@ -138,23 +145,19 @@ function renderContractList(contracts) {
|
|
|
138
145
|
const title = typeof contract.title === 'string' ? contract.title : '<missing-title>';
|
|
139
146
|
const details = [];
|
|
140
147
|
if (Array.isArray(contract.sections) && contract.sections.length > 0) details.push(`sections: ${contract.sections.length}`);
|
|
141
|
-
for (const
|
|
142
|
-
if (Array.isArray(contract[field]) && contract[field].length > 0) details.push(`${label}: ${contract[field].length}`);
|
|
143
|
-
}
|
|
148
|
+
for (const definition of definitions) if (Array.isArray(contract[definition.key]) && contract[definition.key].length > 0) details.push(`${definition.key}: ${contract[definition.key].length}`);
|
|
144
149
|
lines.push(` - \`${escapeCodeSpan(id)}\` — ${escapeMarkdownText(title)}${details.length > 0 ? ` (${details.join(', ')})` : ''}`);
|
|
145
150
|
}
|
|
146
151
|
return lines;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
export function renderTraceabilityLinks(data, root, file) {
|
|
154
|
+
export function renderTraceabilityLinks(data, root, file, config = defaultMemoryConfig()) {
|
|
155
|
+
const definitions = traceabilityKeys(config);
|
|
150
156
|
const body = [
|
|
151
157
|
'### Traceability Links',
|
|
152
158
|
'',
|
|
153
|
-
...
|
|
154
|
-
...
|
|
155
|
-
...renderPathList('Related docs', data?.relatedDocs, root, file),
|
|
156
|
-
...renderCommandList(data?.verificationCommands),
|
|
157
|
-
...renderContractList(data?.contracts),
|
|
159
|
+
...definitions.flatMap((definition) => renderDefinitionList(definition, data?.[definition.key], root, file)),
|
|
160
|
+
...renderContractList(data?.contracts, definitions),
|
|
158
161
|
];
|
|
159
162
|
return `${TRACEABILITY_LINKS_START}\n<!-- generated: do not edit manually -->\n\n${body.join('\n')}\n\n${TRACEABILITY_LINKS_END}`;
|
|
160
163
|
}
|
|
@@ -163,10 +166,10 @@ export function renderCompactTraceabilityBlock(data) {
|
|
|
163
166
|
return `\`\`\`json dotdotgod\n${JSON.stringify(data)}\n\`\`\``;
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
export function syncTraceabilityLinksInContent(content, data, root, file) {
|
|
169
|
+
export function syncTraceabilityLinksInContent(content, data, root, file, config = defaultMemoryConfig()) {
|
|
167
170
|
const region = findTraceabilityLinksRegion(content);
|
|
168
171
|
if (region.status === 'invalid') return { ok: false, changed: false, errors: validateTraceabilityLinksRegion(content, root, file) };
|
|
169
|
-
const generated = renderTraceabilityLinks(data, root, file);
|
|
172
|
+
const generated = renderTraceabilityLinks(data, root, file, config);
|
|
170
173
|
const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
|
|
171
174
|
if (blocks.length === 0) return { ok: false, changed: false, errors: [traceabilityFieldError(rel(root, file), 'TRACEABILITY_LINKS_MISSING_BLOCK', null, 'Cannot generate traceability links without a valid `json dotdotgod` block.')] };
|
|
172
175
|
const block = blocks.at(-1);
|
|
@@ -216,12 +219,19 @@ function validateTraceabilityCommandArray(data, field, add, { required = true, d
|
|
|
216
219
|
for (const value of data[field]) if (typeof value !== 'string' || value.trim().length === 0) add('TRACEABILITY_INVALID_COMMAND', displayField, `invalid command: ${JSON.stringify(value)}.`);
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
function validateConfiguredArray(data, definition, add, root, config, options) {
|
|
223
|
+
if (definition.target === 'command') validateTraceabilityCommandArray(data, definition.key, add, options);
|
|
224
|
+
else validateTraceabilityPathArray(data, definition.key, add, root, config, options);
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
function validateTraceabilityContracts(data, root, add, config) {
|
|
220
228
|
if (data.contracts === undefined) return;
|
|
221
229
|
if (!Array.isArray(data.contracts)) {
|
|
222
230
|
add('TRACEABILITY_INVALID_FIELD', 'contracts', 'must be an array of focused contract objects.');
|
|
223
231
|
return;
|
|
224
232
|
}
|
|
233
|
+
const definitions = traceabilityKeys(config);
|
|
234
|
+
const allowedFields = new Set(['id', 'title', 'sections', ...definitions.map((definition) => definition.key)]);
|
|
225
235
|
const ids = new Set();
|
|
226
236
|
for (const [index, contract] of data.contracts.entries()) {
|
|
227
237
|
const base = `contracts[${index}]`;
|
|
@@ -230,7 +240,7 @@ function validateTraceabilityContracts(data, root, add, config) {
|
|
|
230
240
|
continue;
|
|
231
241
|
}
|
|
232
242
|
for (const field of Object.keys(contract)) {
|
|
233
|
-
if (!
|
|
243
|
+
if (!allowedFields.has(field)) add('TRACEABILITY_INVALID_FIELD', `${base}.${field}`, 'is not a supported contract field.');
|
|
234
244
|
}
|
|
235
245
|
if (typeof contract.id !== 'string' || contract.id.trim().length === 0) {
|
|
236
246
|
add('TRACEABILITY_INVALID_FIELD', `${base}.id`, 'must be a non-empty stable contract ID string.');
|
|
@@ -246,21 +256,22 @@ function validateTraceabilityContracts(data, root, add, config) {
|
|
|
246
256
|
if (typeof section !== 'string' || section.trim().length === 0) add('TRACEABILITY_INVALID_FIELD', `${base}.sections[${sectionIndex}]`, 'must be a non-empty heading title string.');
|
|
247
257
|
}
|
|
248
258
|
}
|
|
249
|
-
for (const
|
|
250
|
-
for (const field of TRACEABILITY_COMMAND_FIELDS) validateTraceabilityCommandArray(contract, field, add, { required: false, displayField: `${base}.${field}` });
|
|
259
|
+
for (const definition of definitions) validateConfiguredArray(contract, definition, add, root, config, { required: false, displayField: `${base}.${definition.key}` });
|
|
251
260
|
}
|
|
252
261
|
}
|
|
253
262
|
|
|
254
263
|
export function validateTraceabilityBlock(data, root, file, line = null, config = defaultMemoryConfig()) {
|
|
255
264
|
const errors = [];
|
|
256
|
-
const add = (code, field, message) => errors.push(traceabilityFieldError(rel(root, file), code, field, message, line));
|
|
265
|
+
const add = (code, field, message) => errors.push(traceabilityFieldError(rel(root, file), code, field, message, line, config));
|
|
257
266
|
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
258
267
|
add('TRACEABILITY_INVALID_JSON', null, 'Traceability block must be a JSON object.');
|
|
259
268
|
return errors;
|
|
260
269
|
}
|
|
261
270
|
if (data.kind !== 'spec') add('TRACEABILITY_INVALID_KIND', 'kind', 'must be "spec" for behavior specs.');
|
|
262
|
-
|
|
263
|
-
|
|
271
|
+
const definitions = traceabilityKeys(config);
|
|
272
|
+
const allowedFields = new Set(['kind', 'contracts', ...definitions.map((definition) => definition.key)]);
|
|
273
|
+
for (const field of Object.keys(data)) if (!allowedFields.has(field)) add('TRACEABILITY_INVALID_FIELD', field, 'is not configured as a traceability field.');
|
|
274
|
+
for (const definition of definitions) validateConfiguredArray(data, definition, add, root, config, { required: true, displayField: definition.key });
|
|
264
275
|
validateTraceabilityContracts(data, root, add, config);
|
|
265
276
|
return errors;
|
|
266
277
|
}
|
|
@@ -43,8 +43,7 @@ function addBounded(list, value, limit) {
|
|
|
43
43
|
export function relationWeight(relation) {
|
|
44
44
|
if (relation === 'implemented_by' || relation === 'verified_by') return 4;
|
|
45
45
|
if (relation === 'includes_resource' || relation === 'routes_to' || relation === 'related_doc' || relation === 'verification_command') return 3;
|
|
46
|
-
if (relation === 'links_to' || relation === 'belongs_to_area' || relation === 'declares_package' || relation === 'declares_bin' || relation === '
|
|
47
|
-
if (relation === 'mentions_package') return 1;
|
|
46
|
+
if (relation === 'links_to' || relation === 'belongs_to_area' || relation === 'declares_package' || relation === 'declares_bin' || relation === 'vector_similarity') return 2;
|
|
48
47
|
return 1;
|
|
49
48
|
}
|
|
50
49
|
|
package/src/graph/metadata.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { basename, extname, resolve } from 'node:path';
|
|
3
4
|
import { rel } from '../common/paths.mjs';
|
|
@@ -72,12 +73,21 @@ function contractNodeId(filePath, contractId) {
|
|
|
72
73
|
return `contract:${filePath}#${contractId}`;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
function traceabilityCommandId(sourceId, key, command) {
|
|
77
|
+
const digest = createHash('sha256').update(`${sourceId}\0${key}\0${command}`).digest('hex').slice(0, 16);
|
|
78
|
+
return `command:${key}:${digest}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function addTraceabilityValues(graph, sourceId, root, filePath, definition, values, config) {
|
|
82
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
83
|
+
if (definition.target === 'path') {
|
|
84
|
+
addTraceabilityTarget(graph, sourceId, root, definition.relation, value, { traceabilityKey: definition.key, relationWeight: definition.weight }, config);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value !== 'string' || value.trim().length === 0) continue;
|
|
88
|
+
const id = traceabilityCommandId(sourceId, definition.key, value);
|
|
89
|
+
addNode(graph, id, 'command', { command: value, path: filePath, traceabilityKey: definition.key, relation: definition.relation });
|
|
90
|
+
addEdge(graph, sourceId, id, definition.relation, { confidence: 'CURATED_TRACEABILITY', traceabilityKey: definition.key, relationWeight: definition.weight });
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -91,10 +101,7 @@ function addTraceabilityContracts(graph, root, fileId, filePath, contracts, conf
|
|
|
91
101
|
const sections = Array.isArray(contract.sections) ? contract.sections.filter((section) => typeof section === 'string' && section.trim().length > 0) : undefined;
|
|
92
102
|
addNode(graph, id, 'contract', { path: filePath, contractId: contract.id, title: contract.title, sections });
|
|
93
103
|
addEdge(graph, fileId, id, 'defines_contract', { confidence: 'CURATED_TRACEABILITY' });
|
|
94
|
-
for (const
|
|
95
|
-
for (const target of Array.isArray(contract.verifiedBy) ? contract.verifiedBy : []) addTraceabilityTarget(graph, id, root, 'verified_by', target, {}, config);
|
|
96
|
-
for (const target of Array.isArray(contract.relatedDocs) ? contract.relatedDocs : []) addTraceabilityTarget(graph, id, root, 'related_doc', target, {}, config);
|
|
97
|
-
addTraceabilityCommands(graph, id, filePath, Array.isArray(contract.verificationCommands) ? contract.verificationCommands : [], id);
|
|
104
|
+
for (const definition of config.traceability?.keys ?? []) addTraceabilityValues(graph, id, root, filePath, definition, contract[definition.key], config);
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
107
|
|
|
@@ -102,10 +109,7 @@ export function addTraceabilityGraph(root, fileId, content, graph, config = defa
|
|
|
102
109
|
const filePath = fileId.replace(/^file:/, '');
|
|
103
110
|
for (const block of extractDotdotgodTraceabilityBlocks(content)) {
|
|
104
111
|
if (block.error || !block.data || block.data.kind !== 'spec') continue;
|
|
105
|
-
for (const
|
|
106
|
-
for (const target of block.data.verifiedBy ?? []) addTraceabilityTarget(graph, fileId, root, 'verified_by', target, {}, config);
|
|
107
|
-
for (const target of block.data.relatedDocs ?? []) addTraceabilityTarget(graph, fileId, root, 'related_doc', target, {}, config);
|
|
108
|
-
addTraceabilityCommands(graph, fileId, filePath, block.data.verificationCommands, fileId);
|
|
112
|
+
for (const definition of config.traceability?.keys ?? []) addTraceabilityValues(graph, fileId, root, filePath, definition, block.data[definition.key], config);
|
|
109
113
|
addTraceabilityContracts(graph, root, fileId, filePath, block.data.contracts, config);
|
|
110
114
|
}
|
|
111
115
|
}
|
package/src/graph/semantic.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export function addDeterministicSemanticEdges(graph, config = defaultMemoryConfi
|
|
|
68
68
|
const threshold = policy.semantic.threshold ?? 0.5;
|
|
69
69
|
const topK = policy.semantic.topKPerFile ?? 5;
|
|
70
70
|
const includeArchiveBodies = policy.semantic.includeArchiveBodies === true;
|
|
71
|
-
const enabledSignals = new Set(policy.semantic.signals ??
|
|
71
|
+
const enabledSignals = new Set(policy.semantic.signals ?? ['path', 'filename', 'heading', 'package']);
|
|
72
72
|
const baseGraph = { nodes: graph.nodes.map((node) => ({ ...node })), edges: graph.edges.filter((edge) => !SEMANTIC_RELATIONS.has(edge.relation)).map((edge) => ({ ...edge })) };
|
|
73
73
|
const files = baseGraph.nodes.filter((node) => node.type === 'file' && node.path);
|
|
74
74
|
const profiles = files.map((node) => semanticProfileForFile(node, baseGraph));
|
package/src/impact/format.mjs
CHANGED
|
@@ -69,8 +69,10 @@ export function formatYmlImpactOutput(payload, impact) {
|
|
|
69
69
|
` omitted_related: ${impact.omittedRelated ?? 0}`,
|
|
70
70
|
' ranking:',
|
|
71
71
|
` method: ${ymlScalar(impact.ranking?.method)}`,
|
|
72
|
-
` preset: ${ymlScalar(impact.ranking?.preset)}`,
|
|
73
72
|
` config_source: ${ymlScalar(impact.ranking?.configSource)}`,
|
|
73
|
+
` connection_cap: ${impact.ranking?.connectionCap ?? 80}`,
|
|
74
|
+
` memory_cap: ${impact.ranking?.memoryCap ?? 20}`,
|
|
75
|
+
` ppr_reference: ${impact.ranking?.pprReference ?? 0.4}`,
|
|
74
76
|
' per_seed:',
|
|
75
77
|
];
|
|
76
78
|
for (const entry of impact.perSeed ?? []) {
|
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
|
}
|