@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/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
package/src/memory/config.mjs
CHANGED
|
@@ -6,9 +6,16 @@ import { isKebabCase } from '../docs/markdown.mjs';
|
|
|
6
6
|
const MEMORY_CONFIG_FILE = 'dotdotgod.config.json';
|
|
7
7
|
const MEMORY_SCOPES = new Set(['shared', 'local']);
|
|
8
8
|
const MEMORY_FRESHNESS = new Set(['fresh', 'stale']);
|
|
9
|
+
export const DEFAULT_TRACEABILITY_KEYS = [
|
|
10
|
+
{ key: 'implementedBy', label: 'Implemented by', description: 'Files that implement the behavior.', target: 'path', relation: 'implemented_by', weight: 4 },
|
|
11
|
+
{ key: 'verifiedBy', label: 'Verified by', description: 'Tests or maintained verification documents.', target: 'path', relation: 'verified_by', weight: 4 },
|
|
12
|
+
{ key: 'relatedDocs', label: 'Related docs', description: 'Documents needed to interpret the behavior.', target: 'path', relation: 'related_doc', weight: 3 },
|
|
13
|
+
{ key: 'verificationCommands', label: 'Verification commands', description: 'Project-local verification commands.', target: 'command', relation: 'verification_command', weight: 3 },
|
|
14
|
+
];
|
|
9
15
|
export const DEFAULT_TRACEABILITY_POLICY = {
|
|
10
16
|
required: ['docs/spec/**'],
|
|
11
17
|
exclude: ['**/README.md'],
|
|
18
|
+
keys: DEFAULT_TRACEABILITY_KEYS,
|
|
12
19
|
};
|
|
13
20
|
export const DEFAULT_VALIDATION_POLICY = {
|
|
14
21
|
markdown: {
|
|
@@ -32,37 +39,27 @@ export const DEFAULT_PLAN_MODE_POLICY = {
|
|
|
32
39
|
writablePaths: ['docs/plan/**', 'docs/archive/**'],
|
|
33
40
|
};
|
|
34
41
|
export const DEFAULT_IMPACT_RANKING_POLICY = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ppr: {
|
|
42
|
+
connectionCap: 80,
|
|
43
|
+
memoryCap: 20,
|
|
44
|
+
ppr: { damping: 0.85, iterations: 20, tolerance: 0.000001, reference: 0.4 },
|
|
38
45
|
relationWeights: {
|
|
39
|
-
implemented_by: 4,
|
|
40
|
-
verified_by: 4,
|
|
41
|
-
related_doc: 3,
|
|
42
|
-
verification_command: 3,
|
|
43
46
|
links_to: 2,
|
|
47
|
+
routes_to: 2,
|
|
44
48
|
belongs_to_area: 2,
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
vector_similarity: 2,
|
|
50
|
+
includes_resource: 1,
|
|
51
|
+
defines_contract: 2,
|
|
47
52
|
},
|
|
48
|
-
|
|
49
|
-
verificationBoosts: { verified_by: 15, 'incoming:verified_by': 15, verification_command: 12, 'incoming:verification_command': 12 },
|
|
50
|
-
semanticBoosts: { semantic_similarity: 8, 'incoming:semantic_similarity': 8, mentions_package: 4, 'incoming:mentions_package': 4 },
|
|
51
|
-
proximityBoosts: { links_to: 6, 'incoming:links_to': 6, routes_to: 5, 'incoming:routes_to': 5 },
|
|
52
|
-
semantic: { enabled: true, threshold: 0.5, topKPerFile: 5, includeArchiveBodies: false, signals: ['path', 'filename', 'heading', 'package'] },
|
|
53
|
-
};
|
|
54
|
-
const IMPACT_RANKING_PRESETS = {
|
|
55
|
-
balanced: {},
|
|
56
|
-
'docs-first': { weights: { ppr: 35, traceability: 35, memoryPolicy: 15, verification: 15, proximity: 5, semantic: 8, freshness: 5, archivePenalty: -30 } },
|
|
57
|
-
'code-proximity': { weights: { ppr: 45, traceability: 20, memoryPolicy: 8, verification: 12, proximity: 20, semantic: 8, freshness: 3, archivePenalty: -25 } },
|
|
58
|
-
'test-focused': { weights: { ppr: 35, traceability: 25, memoryPolicy: 8, verification: 25, proximity: 10, semantic: 7, freshness: 5, archivePenalty: -25 } },
|
|
59
|
-
'archive-aware': { weights: { ppr: 35, traceability: 25, memoryPolicy: 10, verification: 15, proximity: 10, semantic: 8, freshness: 3, archivePenalty: -10 } },
|
|
53
|
+
semantic: { enabled: true, threshold: 0.5, topKPerFile: 5 },
|
|
60
54
|
};
|
|
61
|
-
export const SEMANTIC_RELATIONS = new Set(['
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
55
|
+
export const SEMANTIC_RELATIONS = new Set(['vector_similarity']);
|
|
56
|
+
const TRACEABILITY_TARGETS = new Set(['path', 'command']);
|
|
57
|
+
const TRACEABILITY_RESERVED_KEYS = new Set(['kind', 'contracts', 'id', 'title', 'sections']);
|
|
58
|
+
export const BUILT_IN_GRAPH_RELATIONS = new Set([
|
|
59
|
+
'belongs_to_area', 'includes_resource', 'defines_contract', 'contains_heading', 'links_to', 'routes_to',
|
|
60
|
+
'declares_package', 'declares_script', 'declares_bin', 'depends_on', 'vector_similarity',
|
|
61
|
+
]);
|
|
62
|
+
const TRACEABILITY_RESERVED_RELATIONS = BUILT_IN_GRAPH_RELATIONS;
|
|
66
63
|
const DEFAULT_FUZZY_LOW_SIGNAL_TERMS = [
|
|
67
64
|
'a', 'an', 'and', 'are', 'as', 'by', 'docs', 'document', 'for', 'from', 'it', 'of', 'on', 'plan', 'test', 'the', 'to', 'update', 'version', 'with',
|
|
68
65
|
'계획', '문서', '수정', '업데이트', '버전', '정보', '확인', '테스트',
|
|
@@ -109,13 +106,22 @@ function cloneArea(area) {
|
|
|
109
106
|
}, area);
|
|
110
107
|
}
|
|
111
108
|
|
|
109
|
+
function cloneTraceabilityKey(definition) {
|
|
110
|
+
return { key: definition.key, label: definition.label, description: definition.description, target: definition.target, relation: definition.relation, weight: definition.weight };
|
|
111
|
+
}
|
|
112
|
+
|
|
112
113
|
export function cloneTraceabilityPolicy(policy = DEFAULT_TRACEABILITY_POLICY) {
|
|
113
114
|
return {
|
|
114
115
|
required: [...(policy.required ?? [])],
|
|
115
116
|
exclude: [...(policy.exclude ?? [])],
|
|
117
|
+
keys: (policy.keys ?? DEFAULT_TRACEABILITY_KEYS).map(cloneTraceabilityKey),
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
export function traceabilityRelationWeights(policy = DEFAULT_TRACEABILITY_POLICY) {
|
|
122
|
+
return Object.fromEntries((policy.keys ?? DEFAULT_TRACEABILITY_KEYS).map((definition) => [definition.relation, definition.weight]));
|
|
123
|
+
}
|
|
124
|
+
|
|
119
125
|
export function cloneValidationPolicy(policy = DEFAULT_VALIDATION_POLICY) {
|
|
120
126
|
const filename = policy.markdown?.filename;
|
|
121
127
|
return {
|
|
@@ -132,15 +138,11 @@ export function cloneValidationPolicy(policy = DEFAULT_VALIDATION_POLICY) {
|
|
|
132
138
|
|
|
133
139
|
export function cloneImpactRankingPolicy(policy = DEFAULT_IMPACT_RANKING_POLICY) {
|
|
134
140
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
ppr: { ...DEFAULT_IMPACT_RANKING_POLICY.ppr
|
|
138
|
-
relationWeights: { ...DEFAULT_IMPACT_RANKING_POLICY.relationWeights
|
|
139
|
-
|
|
140
|
-
verificationBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.verificationBoosts, ...(policy.verificationBoosts ?? {}) },
|
|
141
|
-
semanticBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.semanticBoosts, ...(policy.semanticBoosts ?? {}) },
|
|
142
|
-
proximityBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.proximityBoosts, ...(policy.proximityBoosts ?? {}) },
|
|
143
|
-
semantic: { ...DEFAULT_IMPACT_RANKING_POLICY.semantic, ...(policy.semantic ?? {}), signals: [...(policy.semantic?.signals ?? DEFAULT_IMPACT_RANKING_POLICY.semantic.signals)] },
|
|
141
|
+
connectionCap: DEFAULT_IMPACT_RANKING_POLICY.connectionCap,
|
|
142
|
+
memoryCap: DEFAULT_IMPACT_RANKING_POLICY.memoryCap,
|
|
143
|
+
ppr: { ...DEFAULT_IMPACT_RANKING_POLICY.ppr },
|
|
144
|
+
relationWeights: { ...DEFAULT_IMPACT_RANKING_POLICY.relationWeights },
|
|
145
|
+
semantic: { ...DEFAULT_IMPACT_RANKING_POLICY.semantic, ...(policy.semantic ?? {}) },
|
|
144
146
|
};
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -169,9 +171,16 @@ function normalizeReferenceExpansionPolicy(raw) {
|
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
function normalizeImpactRankingPolicy(raw) {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
const candidate = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
175
|
+
&& raw.semantic && typeof raw.semantic === 'object' && !Array.isArray(raw.semantic)
|
|
176
|
+
? raw.semantic
|
|
177
|
+
: {};
|
|
178
|
+
const semantic = {
|
|
179
|
+
...(typeof candidate.enabled === 'boolean' ? { enabled: candidate.enabled } : {}),
|
|
180
|
+
...(isFiniteNumberInRange(candidate.threshold, 0, 1) ? { threshold: candidate.threshold } : {}),
|
|
181
|
+
...(Number.isInteger(candidate.topKPerFile) && candidate.topKPerFile >= 0 && candidate.topKPerFile <= 20 ? { topKPerFile: candidate.topKPerFile } : {}),
|
|
182
|
+
};
|
|
183
|
+
return cloneImpactRankingPolicy({ semantic });
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
@@ -237,6 +246,7 @@ function normalizeTraceabilityPolicy(raw) {
|
|
|
237
246
|
return {
|
|
238
247
|
required: Array.isArray(raw.required) ? raw.required.map(normalizePathPattern) : [],
|
|
239
248
|
exclude: Array.isArray(raw.exclude) ? raw.exclude.map(normalizePathPattern) : [],
|
|
249
|
+
keys: (raw.keys === undefined ? DEFAULT_TRACEABILITY_KEYS : raw.keys).map(cloneTraceabilityKey),
|
|
240
250
|
};
|
|
241
251
|
}
|
|
242
252
|
|
|
@@ -274,8 +284,8 @@ function normalizeLoadPolicy(raw) {
|
|
|
274
284
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneLoadPolicy();
|
|
275
285
|
const normalize = (values) => (Array.isArray(values) ? [...new Set(values.map(normalizePathPattern))] : []);
|
|
276
286
|
return cloneLoadPolicy({
|
|
277
|
-
pinnedPaths:
|
|
278
|
-
pinnedBodies:
|
|
287
|
+
pinnedPaths: [],
|
|
288
|
+
pinnedBodies: [],
|
|
279
289
|
documentationSummary: {
|
|
280
290
|
exclude: raw.documentationSummary?.exclude === undefined
|
|
281
291
|
? DEFAULT_LOAD_POLICY.documentationSummary.exclude
|
|
@@ -314,10 +324,6 @@ function isFiniteNumberInRange(value, min, max) {
|
|
|
314
324
|
return typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
|
|
315
325
|
}
|
|
316
326
|
|
|
317
|
-
function validateNumberMap(map, keys, min, max) {
|
|
318
|
-
return map && typeof map === 'object' && !Array.isArray(map) && Object.entries(map).every(([key, value]) => keys.has(key) && isFiniteNumberInRange(value, min, max));
|
|
319
|
-
}
|
|
320
|
-
|
|
321
327
|
export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.config.json') {
|
|
322
328
|
const errors = [];
|
|
323
329
|
const add = (code, field, message, fix = null) => errors.push({
|
|
@@ -338,6 +344,26 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
338
344
|
else if (traceability.required.some((value) => !isValidPathPattern(value))) add('TRACEABILITY_CONFIG_INVALID_REQUIRED', 'traceability.required', 'Expected path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
339
345
|
if (traceability.exclude !== undefined && !Array.isArray(traceability.exclude)) add('TRACEABILITY_CONFIG_INVALID_EXCLUDE', 'traceability.exclude', 'Expected an array of path strings.');
|
|
340
346
|
else if (Array.isArray(traceability.exclude) && traceability.exclude.some((value) => !isValidPathPattern(value))) add('TRACEABILITY_CONFIG_INVALID_EXCLUDE', 'traceability.exclude', 'Expected path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
347
|
+
if (traceability.keys !== undefined && !Array.isArray(traceability.keys)) add('TRACEABILITY_CONFIG_INVALID_KEYS', 'traceability.keys', 'Expected an array of traceability key definitions.');
|
|
348
|
+
else if (Array.isArray(traceability.keys)) {
|
|
349
|
+
const keys = new Set();
|
|
350
|
+
const labels = new Set();
|
|
351
|
+
const relations = new Set();
|
|
352
|
+
for (const [index, definition] of traceability.keys.entries()) {
|
|
353
|
+
const prefix = `traceability.keys[${index}]`;
|
|
354
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) { add('TRACEABILITY_CONFIG_INVALID_KEY', prefix, 'Expected an object.'); continue; }
|
|
355
|
+
const allowed = new Set(['key', 'label', 'description', 'target', 'relation', 'weight']);
|
|
356
|
+
if (Object.keys(definition).some((key) => !allowed.has(key))) add('TRACEABILITY_CONFIG_INVALID_KEY', prefix, 'Contains unsupported fields.');
|
|
357
|
+
if (typeof definition.key !== 'string' || !/^[A-Za-z][A-Za-z0-9]*$/.test(definition.key) || TRACEABILITY_RESERVED_KEYS.has(definition.key) || keys.has(definition.key)) add('TRACEABILITY_CONFIG_INVALID_KEY', `${prefix}.key`, 'Expected a unique non-reserved identifier.'); else keys.add(definition.key);
|
|
358
|
+
const trimmedLabel = typeof definition.label === 'string' ? definition.label.trim() : '';
|
|
359
|
+
const normalizedLabel = trimmedLabel.toLocaleLowerCase();
|
|
360
|
+
if (!normalizedLabel || /[\u0000-\u001f\u007f]/.test(trimmedLabel) || labels.has(normalizedLabel)) add('TRACEABILITY_CONFIG_INVALID_LABEL', `${prefix}.label`, 'Expected a unique single-line non-empty label after trimming and case normalization.'); else labels.add(normalizedLabel);
|
|
361
|
+
if (typeof definition.description !== 'string' || !definition.description.trim()) add('TRACEABILITY_CONFIG_INVALID_DESCRIPTION', `${prefix}.description`, 'Expected a non-empty description.');
|
|
362
|
+
if (!TRACEABILITY_TARGETS.has(definition.target)) add('TRACEABILITY_CONFIG_INVALID_TARGET', `${prefix}.target`, 'Expected "path" or "command".');
|
|
363
|
+
if (typeof definition.relation !== 'string' || !/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(definition.relation) || TRACEABILITY_RESERVED_RELATIONS.has(definition.relation) || relations.has(definition.relation)) add('TRACEABILITY_CONFIG_INVALID_RELATION', `${prefix}.relation`, 'Expected a unique canonical snake_case relation not reserved by the graph.'); else relations.add(definition.relation);
|
|
364
|
+
if (!isFiniteNumberInRange(definition.weight, 0, 20)) add('TRACEABILITY_CONFIG_INVALID_WEIGHT', `${prefix}.weight`, 'Expected a finite number from 0 to 20.');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
341
367
|
}
|
|
342
368
|
}
|
|
343
369
|
const validation = data.validation;
|
|
@@ -371,15 +397,6 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
371
397
|
if (!load || typeof load !== 'object' || Array.isArray(load)) {
|
|
372
398
|
add('LOAD_CONFIG_INVALID', 'load', 'Expected an object.');
|
|
373
399
|
} else {
|
|
374
|
-
for (const [key, code] of [['pinnedPaths', 'LOAD_CONFIG_INVALID_PINNED_PATHS'], ['pinnedBodies', 'LOAD_CONFIG_INVALID_PINNED_BODIES']]) {
|
|
375
|
-
const values = load[key];
|
|
376
|
-
if (values === undefined) continue;
|
|
377
|
-
if (!Array.isArray(values)) add(code, `load.${key}`, 'Expected an array of path strings.');
|
|
378
|
-
else {
|
|
379
|
-
if (values.some((value) => !isValidPathPattern(value) || (typeof value === 'string' && value.trim().startsWith('/')))) add(code, `load.${key}`, 'Expected repository-relative path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
380
|
-
if (values.some((value) => typeof value === 'string' && isSecretLikePathPattern(value))) add('LOAD_CONFIG_SECRET_PINNED_PATH', `load.${key}`, 'Pinned load paths must not target secrets, credentials, environment files, or private keys.');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
400
|
if (load.documentationSummary !== undefined) {
|
|
384
401
|
const summary = load.documentationSummary;
|
|
385
402
|
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
|
|
@@ -421,40 +438,6 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
421
438
|
}
|
|
422
439
|
}
|
|
423
440
|
}
|
|
424
|
-
const impactRanking = data.impactRanking;
|
|
425
|
-
if (impactRanking !== undefined) {
|
|
426
|
-
if (!impactRanking || typeof impactRanking !== 'object' || Array.isArray(impactRanking)) {
|
|
427
|
-
add('IMPACT_RANKING_CONFIG_INVALID', 'impactRanking', 'Expected an object.');
|
|
428
|
-
} else {
|
|
429
|
-
if (impactRanking.preset !== undefined && !Object.hasOwn(IMPACT_RANKING_PRESETS, impactRanking.preset)) add('IMPACT_RANKING_CONFIG_INVALID_PRESET', 'impactRanking.preset', 'Expected one of balanced, docs-first, code-proximity, test-focused, or archive-aware.');
|
|
430
|
-
if (impactRanking.weights !== undefined && !validateNumberMap(impactRanking.weights, IMPACT_RANKING_WEIGHT_KEYS, -100, 100)) add('IMPACT_RANKING_CONFIG_INVALID_WEIGHTS', 'impactRanking.weights', 'Expected known numeric weight keys with finite values from -100 to 100.');
|
|
431
|
-
if (impactRanking.relationWeights !== undefined && !validateNumberMap(impactRanking.relationWeights, IMPACT_RANKING_RELATION_KEYS, 0, 20)) add('IMPACT_RANKING_CONFIG_INVALID_RELATION_WEIGHTS', 'impactRanking.relationWeights', 'Expected known relation keys with finite values from 0 to 20.');
|
|
432
|
-
for (const key of ['traceabilityBoosts', 'verificationBoosts', 'semanticBoosts', 'proximityBoosts']) {
|
|
433
|
-
if (impactRanking[key] !== undefined && !validateNumberMap(impactRanking[key], IMPACT_RANKING_REASON_KEYS, 0, 100)) add('IMPACT_RANKING_CONFIG_INVALID_BOOSTS', `impactRanking.${key}`, 'Expected known reason keys with finite values from 0 to 100.');
|
|
434
|
-
}
|
|
435
|
-
if (impactRanking.ppr !== undefined) {
|
|
436
|
-
const ppr = impactRanking.ppr;
|
|
437
|
-
if (!ppr || typeof ppr !== 'object' || Array.isArray(ppr)) add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr', 'Expected an object.');
|
|
438
|
-
else {
|
|
439
|
-
if (ppr.enabled !== undefined && typeof ppr.enabled !== 'boolean') add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr.enabled', 'Expected a boolean.');
|
|
440
|
-
if (ppr.damping !== undefined && !isFiniteNumberInRange(ppr.damping, 0.01, 0.99)) add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr.damping', 'Expected a number greater than 0 and less than 1.');
|
|
441
|
-
if (ppr.iterations !== undefined && (!Number.isInteger(ppr.iterations) || ppr.iterations < 1 || ppr.iterations > 100)) add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr.iterations', 'Expected an integer from 1 to 100.');
|
|
442
|
-
if (ppr.tolerance !== undefined && !isFiniteNumberInRange(ppr.tolerance, 0, 1)) add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr.tolerance', 'Expected a number from 0 to 1.');
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
if (impactRanking.semantic !== undefined) {
|
|
446
|
-
const semantic = impactRanking.semantic;
|
|
447
|
-
if (!semantic || typeof semantic !== 'object' || Array.isArray(semantic)) add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic', 'Expected an object.');
|
|
448
|
-
else {
|
|
449
|
-
if (semantic.enabled !== undefined && typeof semantic.enabled !== 'boolean') add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.enabled', 'Expected a boolean.');
|
|
450
|
-
if (semantic.threshold !== undefined && !isFiniteNumberInRange(semantic.threshold, 0, 1)) add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.threshold', 'Expected a number from 0 to 1.');
|
|
451
|
-
if (semantic.topKPerFile !== undefined && (!Number.isInteger(semantic.topKPerFile) || semantic.topKPerFile < 0 || semantic.topKPerFile > 20)) add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.topKPerFile', 'Expected an integer from 0 to 20.');
|
|
452
|
-
if (semantic.includeArchiveBodies !== undefined && typeof semantic.includeArchiveBodies !== 'boolean') add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.includeArchiveBodies', 'Expected a boolean.');
|
|
453
|
-
if (semantic.signals !== undefined && (!Array.isArray(semantic.signals) || semantic.signals.some((value) => !SEMANTIC_SIGNAL_KEYS.has(value)))) add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.signals', 'Expected an array of known deterministic signal names.');
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
441
|
const areas = data.memory?.areas;
|
|
459
442
|
if (areas === undefined) return errors;
|
|
460
443
|
if (!Array.isArray(areas)) {
|
|
@@ -540,7 +523,7 @@ export function defaultDotdotgodConfigData() {
|
|
|
540
523
|
},
|
|
541
524
|
traceability: cloneTraceabilityPolicy(config.traceability),
|
|
542
525
|
validation: cloneValidationPolicy(config.validation),
|
|
543
|
-
impactRanking:
|
|
526
|
+
impactRanking: { semantic: { ...config.impactRanking.semantic } },
|
|
544
527
|
referenceExpansion: { fuzzy: { lowSignal: { add: [], remove: [] } } },
|
|
545
528
|
load: cloneLoadPolicy(config.load),
|
|
546
529
|
planMode: clonePlanModePolicy(config.planMode),
|
|
@@ -567,7 +550,15 @@ export function memoryConfigSummary(config) {
|
|
|
567
550
|
}, area)),
|
|
568
551
|
traceability: cloneTraceabilityPolicy(config.traceability ?? DEFAULT_TRACEABILITY_POLICY),
|
|
569
552
|
validation: cloneValidationPolicy(config.validation ?? DEFAULT_VALIDATION_POLICY),
|
|
570
|
-
impactRanking:
|
|
553
|
+
impactRanking: {
|
|
554
|
+
fixed: {
|
|
555
|
+
connectionCap: DEFAULT_IMPACT_RANKING_POLICY.connectionCap,
|
|
556
|
+
memoryCap: DEFAULT_IMPACT_RANKING_POLICY.memoryCap,
|
|
557
|
+
ppr: { ...DEFAULT_IMPACT_RANKING_POLICY.ppr },
|
|
558
|
+
relationWeights: { ...DEFAULT_IMPACT_RANKING_POLICY.relationWeights },
|
|
559
|
+
},
|
|
560
|
+
semantic: { ...cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY).semantic },
|
|
561
|
+
},
|
|
571
562
|
referenceExpansion: cloneReferenceExpansionPolicy(config.referenceExpansion),
|
|
572
563
|
load: cloneLoadPolicy(config.load),
|
|
573
564
|
planMode: clonePlanModePolicy(config.planMode),
|