@dotdotgod/cli 0.2.1 → 0.2.2
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 +5 -3
- package/package.json +1 -1
- package/src/commands/config.mjs +2 -0
- package/src/commands/load-snapshot.mjs +11 -5
- package/src/core.mjs +2 -2
- package/src/index/files.mjs +1 -1
- package/src/load-snapshot/summary.mjs +96 -2
- package/src/memory/config.mjs +39 -3
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @dotdotgod/cli
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@dotdotgod/cli) [](https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli) [](
|
|
3
|
+
[](https://www.npmjs.com/package/@dotdotgod/cli) [](https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli) [](https://github.com/dotdotgod/dotdotgod-kit/blob/main/LICENSE)
|
|
4
4
|
|
|
5
5
|
Command-line tools for dotdotgod project memory.
|
|
6
6
|
|
|
7
7
|
Use this package when you want to initialize a docs-first project scaffold, validate project-memory docs, build a local graph/cache, load bounded context snapshots, expand project references, or ask what should be checked after a file changes.
|
|
8
8
|
|
|
9
|
+
These commands implement dotdotgod's project-memory loop — init, load, plan, impact — for shells, scripts, CI, and any agent without a dedicated adapter.
|
|
10
|
+
|
|
9
11
|
## Start Here
|
|
10
12
|
|
|
11
13
|
Run one command without installing globally:
|
|
@@ -34,7 +36,7 @@ dotdotgod graph impact . --changed <path> --compact
|
|
|
34
36
|
- `init` creates `AGENTS.md`, thin agent entrypoints, docs indexes, active-plan space, archive map, and local cache ignores.
|
|
35
37
|
- `validate` checks the dotdotgod docs/project-memory structure, local links, traceability blocks, config validity, and optional index freshness.
|
|
36
38
|
- `index` builds `.dotdotgod/manifest.json` and compact graph shards from maintained project files.
|
|
37
|
-
- `load-snapshot` returns a bounded first-pass project map for AI agents.
|
|
39
|
+
- `load-snapshot` returns a bounded first-pass project map for AI agents, including files pinned through the `load.pinnedPaths`/`load.pinnedBodies` config so context such as code conventions stays visible on every load.
|
|
38
40
|
- `resolve` and `expand` map explicit or high-signal prompt references to project files.
|
|
39
41
|
- `graph impact` ranks likely related specs, tests, docs, commands, and source files for a changed path.
|
|
40
42
|
- `traceability links` checks or repairs generated Markdown traceability-link sections.
|
|
@@ -139,4 +141,4 @@ Default exclusions include dependency, generated, cache, and secret-like paths s
|
|
|
139
141
|
|
|
140
142
|
## Learn More
|
|
141
143
|
|
|
142
|
-
See the [root README](
|
|
144
|
+
See the [root README](https://github.com/dotdotgod/dotdotgod-kit#readme), [Context curation](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/concept/CONTEXT_CURATION.md), [Context mechanics](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/concept/CONTEXT_MECHANICS.md), [Memory area config](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/spec/MEMORY_AREA_CONFIG.md), and [Traceability config](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/spec/TRACEABILITY_CONFIG.md).
|
package/package.json
CHANGED
package/src/commands/config.mjs
CHANGED
|
@@ -44,6 +44,8 @@ function formatConfigOutput(payload) {
|
|
|
44
44
|
lines.push(`- validation markdown: maxLines=${payload.config.validation.markdown.maxLines}, maxChars=${payload.config.validation.markdown.maxChars}`);
|
|
45
45
|
lines.push(`- validation markdown exclude: ${(payload.config.validation.markdown.exclude ?? []).join(', ') || 'none'}`);
|
|
46
46
|
lines.push(`- impact ranking preset: ${payload.config.impactRanking.preset}`);
|
|
47
|
+
lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(', ') || 'none'}`);
|
|
48
|
+
lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(', ') || 'none'}`);
|
|
47
49
|
lines.push(`- trello sync paths: ${(payload.config.integrations?.trello?.syncPaths ?? []).join(', ') || 'none'}`);
|
|
48
50
|
const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
|
|
49
51
|
lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(', ') || 'none'} (add: ${(lowSignal.add ?? []).join(', ') || 'none'}; remove: ${(lowSignal.remove ?? []).join(', ') || 'none'})`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseCommon } from '../cli/usage.mjs';
|
|
2
2
|
import { buildCommunities } from '../graph/communities.mjs';
|
|
3
3
|
import { memoryConfigSummary, readMemoryConfig } from '../memory/config.mjs';
|
|
4
|
-
import { buildMemoryAreas, detectCommandGuidance } from '../load-snapshot/summary.mjs';
|
|
4
|
+
import { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance } from '../load-snapshot/summary.mjs';
|
|
5
5
|
import { graphSummary, readFreshIndex } from '../index/cache.mjs';
|
|
6
6
|
|
|
7
7
|
export function runLoadSnapshot(argv) {
|
|
@@ -10,7 +10,9 @@ export function runLoadSnapshot(argv) {
|
|
|
10
10
|
const summary = graphSummary(index);
|
|
11
11
|
const communities = buildCommunities(index, { communities: 5, items: 5 });
|
|
12
12
|
const memoryAreas = buildMemoryAreas(index, { items: 4 });
|
|
13
|
-
const
|
|
13
|
+
const resolvedConfig = readMemoryConfig(options.root);
|
|
14
|
+
const memoryConfig = index.memoryConfig ?? memoryConfigSummary(resolvedConfig);
|
|
15
|
+
const pinnedFiles = buildPinnedFiles(options.root, resolvedConfig);
|
|
14
16
|
const memoryPolicy = {
|
|
15
17
|
source: memoryConfig.source ?? 'default',
|
|
16
18
|
sharedAreas: (memoryConfig.areas ?? []).filter((area) => area.scope === 'shared').map((area) => area.id),
|
|
@@ -18,7 +20,7 @@ export function runLoadSnapshot(argv) {
|
|
|
18
20
|
freshAreas: (memoryConfig.areas ?? []).filter((area) => area.freshness === 'fresh').map((area) => area.id),
|
|
19
21
|
staleAreas: (memoryConfig.areas ?? []).filter((area) => area.freshness === 'stale').map((area) => area.id),
|
|
20
22
|
};
|
|
21
|
-
const bounds = { communities: 5, communityItems: 5, memoryAreaItems: 4, fullGraphIncluded: false, archiveBodiesIncluded: status.archiveBodiesIncluded, archiveMapIncluded: true };
|
|
23
|
+
const bounds = { communities: 5, communityItems: 5, memoryAreaItems: 4, pinnedPathItems: pinnedFiles.bounds.maxPaths, pinnedBodyItems: pinnedFiles.bounds.maxBodies, pinnedBodyChars: pinnedFiles.bounds.maxBodyChars, fullGraphIncluded: false, archiveBodiesIncluded: status.archiveBodiesIncluded, archiveMapIncluded: true };
|
|
22
24
|
const quality = {
|
|
23
25
|
indexedFiles: status.indexedFiles,
|
|
24
26
|
currentFiles: status.currentFiles,
|
|
@@ -28,13 +30,17 @@ export function runLoadSnapshot(argv) {
|
|
|
28
30
|
omittedCommunityItems: communities.communities.reduce((sum, community) => sum + (community.omitted ?? 0), 0),
|
|
29
31
|
memoryAreas: memoryAreas.total,
|
|
30
32
|
omittedMemoryAreaItems: memoryAreas.areas.reduce((sum, area) => sum + (area.omitted ?? 0), 0),
|
|
33
|
+
pinnedPaths: pinnedFiles.paths.length,
|
|
34
|
+
omittedPinnedPaths: pinnedFiles.omittedPaths,
|
|
35
|
+
pinnedBodies: pinnedFiles.bodies.length,
|
|
36
|
+
omittedPinnedBodies: pinnedFiles.omittedBodies,
|
|
31
37
|
graphNodes: summary.nodes,
|
|
32
38
|
graphEdges: summary.edges,
|
|
33
39
|
};
|
|
34
40
|
const commandGuidance = detectCommandGuidance(options.root);
|
|
35
|
-
let payload = { root: options.root, cache: status, metadata, graph: summary, memoryConfig, memoryPolicy, memoryAreas, communities, bounds, quality, commandGuidance };
|
|
41
|
+
let payload = { root: options.root, cache: status, metadata, graph: summary, memoryConfig, memoryPolicy, memoryAreas, pinnedFiles, communities, bounds, quality, commandGuidance };
|
|
36
42
|
const serialized = JSON.stringify(payload);
|
|
37
43
|
payload = { ...payload, quality: { ...quality, snapshotBytes: Buffer.byteLength(serialized), approxSnapshotTokens: Math.ceil(serialized.length / 4) } };
|
|
38
44
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
39
|
-
else console.log(`dotdotgod load snapshot\n- cache: ${status.status}${metadata.cacheRefreshed ? ' (refreshed)' : ''}\n- indexed files: ${status.indexedFiles}\n- current files: ${status.currentFiles}\n- archive bodies included: ${status.archiveBodiesIncluded ? 'yes' : 'no'}\n- graph: ${summary.nodes} nodes, ${summary.edges} edges\n- memory areas: ${memoryAreas.areas.length}/${memoryAreas.total} shown\n- communities: ${communities.communities.length}/${communities.total} shown, ${communities.omitted} omitted`);
|
|
45
|
+
else console.log(`dotdotgod load snapshot\n- cache: ${status.status}${metadata.cacheRefreshed ? ' (refreshed)' : ''}\n- indexed files: ${status.indexedFiles}\n- current files: ${status.currentFiles}\n- archive bodies included: ${status.archiveBodiesIncluded ? 'yes' : 'no'}\n- graph: ${summary.nodes} nodes, ${summary.edges} edges\n- memory areas: ${memoryAreas.areas.length}/${memoryAreas.total} shown\n- pinned files: ${pinnedFiles.paths.length} paths (${pinnedFiles.omittedPaths} omitted), ${pinnedFiles.bodies.length} bodies (${pinnedFiles.omittedBodies} omitted)\n- communities: ${communities.communities.length}/${communities.total} shown, ${communities.omitted} omitted`);
|
|
40
46
|
}
|
package/src/core.mjs
CHANGED
|
@@ -15,11 +15,11 @@ export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersion
|
|
|
15
15
|
export { rel } from './common/paths.mjs';
|
|
16
16
|
export { extractAnchors, extractFirstHeading, extractLinks, headingToAnchor, isKebabCase, isNumberedSeriesFilename, isUpperSnakeMarkdown, removeCodeBlocks, suggestFilenameFromHeading } from './docs/markdown.mjs';
|
|
17
17
|
export { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
|
|
18
|
-
export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_INTEGRATIONS_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneIntegrationsPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, memoryRoleForPath, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, retrievalPriorityForPath, trelloSyncPaths, validateMemoryConfigData } from './memory/config.mjs';
|
|
18
|
+
export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_INTEGRATIONS_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneIntegrationsPolicy, cloneLoadPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, isSecretLikePathPattern, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, memoryRoleForPath, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, retrievalPriorityForPath, trelloSyncPaths, validateMemoryConfigData } from './memory/config.mjs';
|
|
19
19
|
export { buildCommunities, relationWeight } from './graph/communities.mjs';
|
|
20
20
|
export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
|
|
21
21
|
export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
|
|
22
|
-
export { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from './load-snapshot/summary.mjs';
|
|
22
|
+
export { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance, detectPackageManager } from './load-snapshot/summary.mjs';
|
|
23
23
|
export { isReadmeIndexPath, retrievalMetadataForPath, fileNodeMetadata } from './graph/metadata.mjs';
|
|
24
24
|
export { extractMarkdownGraph, extractPackageGraph, buildGraph } from './graph/extract.mjs';
|
|
25
25
|
export { addDeterministicSemanticEdges } from './graph/semantic.mjs';
|
package/src/index/files.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const INDEX_TEXT_FILENAMES = new Set([
|
|
|
21
21
|
]);
|
|
22
22
|
const INDEX_EXCLUDED_DIRS = new Set(['.git', CACHE_DIR, 'node_modules', 'vendor', '.venv', 'venv', 'target', 'dist', 'build', 'coverage', '.next', '.turbo', '.cache', '.pytest_cache', '__pycache__']);
|
|
23
23
|
|
|
24
|
-
function isExcludedIndexDir(path) {
|
|
24
|
+
export function isExcludedIndexDir(path) {
|
|
25
25
|
return path.split('/').some((part) => INDEX_EXCLUDED_DIRS.has(part));
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { rel } from '../common/paths.mjs';
|
|
4
|
+
import { isExcludedIndexDir } from '../index/files.mjs';
|
|
5
|
+
import { defaultMemoryConfig, isSecretLikePathPattern, matchPathPattern, memoryAreaForPath } from '../memory/config.mjs';
|
|
4
6
|
|
|
5
7
|
export function detectPackageManager(root) {
|
|
6
8
|
const packageFile = join(root, 'package.json');
|
|
@@ -48,6 +50,98 @@ export function detectCommandGuidance(root) {
|
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
const PINNED_WALK_CAP = 200;
|
|
54
|
+
|
|
55
|
+
function walkPinnedMatches(root, base, matches, cap) {
|
|
56
|
+
if (!existsSync(base)) return [];
|
|
57
|
+
const results = [];
|
|
58
|
+
const walk = (dir) => {
|
|
59
|
+
if (results.length >= cap) return;
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (results.length >= cap) return;
|
|
69
|
+
const absolute = join(dir, entry.name);
|
|
70
|
+
const path = rel(root, absolute);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (!isExcludedIndexDir(path)) walk(absolute);
|
|
73
|
+
} else if (entry.isFile() && matches(path)) results.push(path);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
walk(base);
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function expandPinnedPattern(root, pattern) {
|
|
81
|
+
if (!pattern.includes('*')) {
|
|
82
|
+
try {
|
|
83
|
+
return statSync(join(root, pattern)).isFile() ? [pattern] : [];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const base = pattern.endsWith('/**') ? join(root, pattern.slice(0, -3)) : root;
|
|
89
|
+
return walkPinnedMatches(root, base, (path) => !isSecretLikePathPattern(path) && matchPathPattern(path, pattern), PINNED_WALK_CAP);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readPinnedBody(root, path, maxBodyChars) {
|
|
93
|
+
if (isSecretLikePathPattern(path)) return { path, status: 'skipped', reason: 'secret-like-path' };
|
|
94
|
+
let content;
|
|
95
|
+
try {
|
|
96
|
+
content = readFileSync(join(root, path));
|
|
97
|
+
} catch {
|
|
98
|
+
return { path, status: 'missing' };
|
|
99
|
+
}
|
|
100
|
+
if (content.includes(0)) return { path, status: 'skipped', reason: 'binary' };
|
|
101
|
+
const text = content.toString('utf8');
|
|
102
|
+
const truncated = text.length > maxBodyChars;
|
|
103
|
+
return { path, status: truncated ? 'truncated' : 'present', truncated, chars: text.length, content: truncated ? text.slice(0, maxBodyChars) : text };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildPinnedFiles(root, config = defaultMemoryConfig(), limits = {}) {
|
|
107
|
+
const policy = config.load ?? { pinnedPaths: [], pinnedBodies: [] };
|
|
108
|
+
const maxPaths = limits.paths ?? 20;
|
|
109
|
+
const maxBodies = limits.bodies ?? 5;
|
|
110
|
+
const maxBodyChars = limits.bodyChars ?? 10000;
|
|
111
|
+
const entries = new Map();
|
|
112
|
+
const addMatches = (pattern, kind) => {
|
|
113
|
+
const matches = expandPinnedPattern(root, pattern);
|
|
114
|
+
for (const path of matches.length > 0 ? matches : [pattern]) {
|
|
115
|
+
const entry = entries.get(path) ?? { path, status: matches.length > 0 ? 'present' : 'missing', pinnedBy: new Set() };
|
|
116
|
+
entry.pinnedBy.add(kind);
|
|
117
|
+
entries.set(path, entry);
|
|
118
|
+
}
|
|
119
|
+
return matches;
|
|
120
|
+
};
|
|
121
|
+
for (const pattern of policy.pinnedPaths ?? []) addMatches(pattern, 'pinnedPaths');
|
|
122
|
+
const bodyCandidates = new Set();
|
|
123
|
+
for (const pattern of policy.pinnedBodies ?? []) {
|
|
124
|
+
for (const path of addMatches(pattern, 'pinnedBodies')) bodyCandidates.add(path);
|
|
125
|
+
}
|
|
126
|
+
const allPaths = [...entries.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
127
|
+
const paths = allPaths.slice(0, maxPaths).map((entry) => ({ path: entry.path, status: entry.status, pinnedBy: [...entry.pinnedBy].sort() }));
|
|
128
|
+
const bodies = [];
|
|
129
|
+
let omittedBodies = 0;
|
|
130
|
+
for (const path of [...bodyCandidates].sort()) {
|
|
131
|
+
if (bodies.length >= maxBodies) omittedBodies += 1;
|
|
132
|
+
else bodies.push(readPinnedBody(root, path, maxBodyChars));
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
source: config.source ?? 'default',
|
|
136
|
+
configured: { pinnedPaths: [...(policy.pinnedPaths ?? [])], pinnedBodies: [...(policy.pinnedBodies ?? [])] },
|
|
137
|
+
paths,
|
|
138
|
+
omittedPaths: allPaths.length - paths.length,
|
|
139
|
+
bodies,
|
|
140
|
+
omittedBodies,
|
|
141
|
+
bounds: { maxPaths, maxBodies, maxBodyChars },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
51
145
|
export function buildMemoryAreas(index, limits = {}) {
|
|
52
146
|
const graph = index?.graph ?? { nodes: [], edges: [] };
|
|
53
147
|
const itemLimit = limits.items ?? 4;
|
package/src/memory/config.mjs
CHANGED
|
@@ -24,6 +24,10 @@ export const DEFAULT_VALIDATION_POLICY = {
|
|
|
24
24
|
export const DEFAULT_INTEGRATIONS_POLICY = {
|
|
25
25
|
trello: { syncPaths: [] },
|
|
26
26
|
};
|
|
27
|
+
export const DEFAULT_LOAD_POLICY = {
|
|
28
|
+
pinnedPaths: [],
|
|
29
|
+
pinnedBodies: [],
|
|
30
|
+
};
|
|
27
31
|
export const DEFAULT_IMPACT_RANKING_POLICY = {
|
|
28
32
|
preset: 'balanced',
|
|
29
33
|
weights: { ppr: 40, traceability: 30, memoryPolicy: 10, verification: 15, proximity: 10, semantic: 10, freshness: 5, archivePenalty: -25 },
|
|
@@ -170,8 +174,15 @@ export function cloneIntegrationsPolicy(policy = DEFAULT_INTEGRATIONS_POLICY) {
|
|
|
170
174
|
return { trello: { syncPaths: [...(policy.trello?.syncPaths ?? [])] } };
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
178
|
+
return {
|
|
179
|
+
pinnedPaths: [...(policy.pinnedPaths ?? [])],
|
|
180
|
+
pinnedBodies: [...(policy.pinnedBodies ?? [])],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
export function defaultMemoryConfig() {
|
|
174
|
-
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy() };
|
|
185
|
+
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy(), load: cloneLoadPolicy() };
|
|
175
186
|
}
|
|
176
187
|
|
|
177
188
|
export function normalizePathPattern(value = '') {
|
|
@@ -253,11 +264,17 @@ function normalizeIntegrationsPolicy(raw) {
|
|
|
253
264
|
return cloneIntegrationsPolicy({ trello: { syncPaths: [...new Set(syncPaths)] } });
|
|
254
265
|
}
|
|
255
266
|
|
|
256
|
-
function isSecretLikePathPattern(value = '') {
|
|
267
|
+
export function isSecretLikePathPattern(value = '') {
|
|
257
268
|
const normalized = normalizePathPattern(value);
|
|
258
269
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
|
|
259
270
|
}
|
|
260
271
|
|
|
272
|
+
function normalizeLoadPolicy(raw) {
|
|
273
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneLoadPolicy();
|
|
274
|
+
const normalize = (values) => (Array.isArray(values) ? [...new Set(values.map(normalizePathPattern))] : []);
|
|
275
|
+
return cloneLoadPolicy({ pinnedPaths: normalize(raw.pinnedPaths), pinnedBodies: normalize(raw.pinnedBodies) });
|
|
276
|
+
}
|
|
277
|
+
|
|
261
278
|
export function trelloSyncPaths(config = defaultMemoryConfig()) {
|
|
262
279
|
return [...new Set(['docs/trello/**', ...((config.integrations ?? DEFAULT_INTEGRATIONS_POLICY).trello?.syncPaths ?? [])])];
|
|
263
280
|
}
|
|
@@ -361,6 +378,22 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
361
378
|
}
|
|
362
379
|
}
|
|
363
380
|
}
|
|
381
|
+
const load = data.load;
|
|
382
|
+
if (load !== undefined) {
|
|
383
|
+
if (!load || typeof load !== 'object' || Array.isArray(load)) {
|
|
384
|
+
add('LOAD_CONFIG_INVALID', 'load', 'Expected an object.');
|
|
385
|
+
} else {
|
|
386
|
+
for (const [key, code] of [['pinnedPaths', 'LOAD_CONFIG_INVALID_PINNED_PATHS'], ['pinnedBodies', 'LOAD_CONFIG_INVALID_PINNED_BODIES']]) {
|
|
387
|
+
const values = load[key];
|
|
388
|
+
if (values === undefined) continue;
|
|
389
|
+
if (!Array.isArray(values)) add(code, `load.${key}`, 'Expected an array of path strings.');
|
|
390
|
+
else {
|
|
391
|
+
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.');
|
|
392
|
+
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.');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
364
397
|
const referenceExpansion = data.referenceExpansion;
|
|
365
398
|
if (referenceExpansion !== undefined) {
|
|
366
399
|
if (!referenceExpansion || typeof referenceExpansion !== 'object' || Array.isArray(referenceExpansion)) {
|
|
@@ -472,7 +505,8 @@ export function readMemoryConfig(root = '.') {
|
|
|
472
505
|
const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
|
|
473
506
|
const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
|
|
474
507
|
const integrations = normalizeIntegrationsPolicy(data.integrations);
|
|
475
|
-
|
|
508
|
+
const load = normalizeLoadPolicy(data.load);
|
|
509
|
+
return configuredAreas.length > 0 ? { source: name, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, integrations, load, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, integrations, load, source: name, errors: [] };
|
|
476
510
|
} catch (error) {
|
|
477
511
|
return { ...defaultMemoryConfig(), source: name, errors: [{ file: name, code: 'MEMORY_CONFIG_INVALID_JSON', message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}\nFix: repair ${name} so it is valid JSON, or regenerate the default config with \`dotdotgod config init <root> --force\` if you want to reset it.` }] };
|
|
478
512
|
}
|
|
@@ -505,6 +539,7 @@ export function defaultDotdotgodConfigData() {
|
|
|
505
539
|
impactRanking: cloneImpactRankingPolicy(config.impactRanking),
|
|
506
540
|
referenceExpansion: { fuzzy: { lowSignal: { add: [], remove: [] } } },
|
|
507
541
|
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
542
|
+
load: cloneLoadPolicy(config.load),
|
|
508
543
|
};
|
|
509
544
|
}
|
|
510
545
|
|
|
@@ -531,6 +566,7 @@ export function memoryConfigSummary(config) {
|
|
|
531
566
|
impactRanking: cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY),
|
|
532
567
|
referenceExpansion: cloneReferenceExpansionPolicy(config.referenceExpansion),
|
|
533
568
|
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
569
|
+
load: cloneLoadPolicy(config.load),
|
|
534
570
|
};
|
|
535
571
|
}
|
|
536
572
|
|