@dotdotgod/cli 0.2.0 → 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 CHANGED
@@ -1,11 +1,13 @@
1
1
  # @dotdotgod/cli
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@dotdotgod/cli.svg)](https://www.npmjs.com/package/@dotdotgod/cli) [![GitHub](https://img.shields.io/badge/GitHub-dotdotgod%2Fdotdotgod--kit-181717?logo=github)](https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli) [![License: Elastic 2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](../../LICENSE)
3
+ [![npm version](https://img.shields.io/npm/v/@dotdotgod/cli.svg)](https://www.npmjs.com/package/@dotdotgod/cli) [![GitHub](https://img.shields.io/badge/GitHub-dotdotgod%2Fdotdotgod--kit-181717?logo=github)](https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli) [![License: Elastic 2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](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](../../README.md), [GitHub repository](https://github.com/dotdotgod/dotdotgod-kit), [Context curation](../../docs/concept/CONTEXT_CURATION.md), [Context mechanics](../../docs/concept/CONTEXT_MECHANICS.md), [Memory area config](../../docs/spec/MEMORY_AREA_CONFIG.md), and [Traceability config](../../docs/spec/TRACEABILITY_CONFIG.md).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for dotdotgod validation, project memory snapshots, and graph indexing.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -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 memoryConfig = index.memoryConfig ?? memoryConfigSummary(readMemoryConfig(options.root));
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';
@@ -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 { defaultMemoryConfig, memoryAreaForPath } from '../memory/config.mjs';
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;
@@ -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
- return configuredAreas.length > 0 ? { source: name, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, integrations, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, integrations, source: name, errors: [] };
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