@dotdotgod/cli 0.2.14 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: {
@@ -21,9 +28,6 @@ export const DEFAULT_VALIDATION_POLICY = {
21
28
  },
22
29
  },
23
30
  };
24
- export const DEFAULT_INTEGRATIONS_POLICY = {
25
- trello: { syncPaths: [] },
26
- };
27
31
  export const DEFAULT_LOAD_POLICY = {
28
32
  pinnedPaths: [],
29
33
  pinnedBodies: [],
@@ -35,37 +39,27 @@ export const DEFAULT_PLAN_MODE_POLICY = {
35
39
  writablePaths: ['docs/plan/**', 'docs/archive/**'],
36
40
  };
37
41
  export const DEFAULT_IMPACT_RANKING_POLICY = {
38
- preset: 'balanced',
39
- weights: { ppr: 40, traceability: 30, memoryPolicy: 10, verification: 15, proximity: 10, semantic: 10, freshness: 5, archivePenalty: -25 },
40
- ppr: { enabled: true, damping: 0.85, iterations: 20, tolerance: 0.000001 },
42
+ connectionCap: 80,
43
+ memoryCap: 20,
44
+ ppr: { damping: 0.85, iterations: 20, tolerance: 0.000001, reference: 0.4 },
41
45
  relationWeights: {
42
- implemented_by: 4,
43
- verified_by: 4,
44
- related_doc: 3,
45
- verification_command: 3,
46
46
  links_to: 2,
47
+ routes_to: 2,
47
48
  belongs_to_area: 2,
48
- semantic_similarity: 2,
49
- mentions_package: 1,
49
+ vector_similarity: 2,
50
+ includes_resource: 1,
51
+ defines_contract: 2,
50
52
  },
51
- traceabilityBoosts: { implemented_by: 30, 'incoming:implemented_by': 30, verified_by: 25, 'incoming:verified_by': 25, verification_command: 15, 'incoming:verification_command': 15, related_doc: 12, 'incoming:related_doc': 12 },
52
- verificationBoosts: { verified_by: 15, 'incoming:verified_by': 15, verification_command: 12, 'incoming:verification_command': 12 },
53
- semanticBoosts: { semantic_similarity: 8, 'incoming:semantic_similarity': 8, mentions_package: 4, 'incoming:mentions_package': 4 },
54
- proximityBoosts: { links_to: 6, 'incoming:links_to': 6, routes_to: 5, 'incoming:routes_to': 5 },
55
- semantic: { enabled: true, threshold: 0.5, topKPerFile: 5, includeArchiveBodies: false, signals: ['path', 'filename', 'heading', 'package'] },
56
- };
57
- const IMPACT_RANKING_PRESETS = {
58
- balanced: {},
59
- 'docs-first': { weights: { ppr: 35, traceability: 35, memoryPolicy: 15, verification: 15, proximity: 5, semantic: 8, freshness: 5, archivePenalty: -30 } },
60
- 'code-proximity': { weights: { ppr: 45, traceability: 20, memoryPolicy: 8, verification: 12, proximity: 20, semantic: 8, freshness: 3, archivePenalty: -25 } },
61
- 'test-focused': { weights: { ppr: 35, traceability: 25, memoryPolicy: 8, verification: 25, proximity: 10, semantic: 7, freshness: 5, archivePenalty: -25 } },
62
- '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 },
63
54
  };
64
- export const SEMANTIC_RELATIONS = new Set(['semantic_similarity', 'mentions_package']);
65
- const IMPACT_RANKING_WEIGHT_KEYS = new Set(['ppr', 'traceability', 'memoryPolicy', 'verification', 'proximity', 'semantic', 'freshness', 'archivePenalty']);
66
- const IMPACT_RANKING_RELATION_KEYS = new Set(['implemented_by', 'verified_by', 'related_doc', 'verification_command', 'links_to', 'belongs_to_area', 'semantic_similarity', 'mentions_package']);
67
- const IMPACT_RANKING_REASON_KEYS = new Set(['implemented_by', 'incoming:implemented_by', 'verified_by', 'incoming:verified_by', 'verification_command', 'incoming:verification_command', 'related_doc', 'incoming:related_doc', 'semantic_similarity', 'incoming:semantic_similarity', 'mentions_package', 'incoming:mentions_package', 'links_to', 'incoming:links_to', 'routes_to', 'incoming:routes_to']);
68
- const SEMANTIC_SIGNAL_KEYS = new Set(['path', 'filename', 'heading', 'package']);
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;
69
63
  const DEFAULT_FUZZY_LOW_SIGNAL_TERMS = [
70
64
  'a', 'an', 'and', 'are', 'as', 'by', 'docs', 'document', 'for', 'from', 'it', 'of', 'on', 'plan', 'test', 'the', 'to', 'update', 'version', 'with',
71
65
  '계획', '문서', '수정', '업데이트', '버전', '정보', '확인', '테스트',
@@ -112,13 +106,22 @@ function cloneArea(area) {
112
106
  }, area);
113
107
  }
114
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
+
115
113
  export function cloneTraceabilityPolicy(policy = DEFAULT_TRACEABILITY_POLICY) {
116
114
  return {
117
115
  required: [...(policy.required ?? [])],
118
116
  exclude: [...(policy.exclude ?? [])],
117
+ keys: (policy.keys ?? DEFAULT_TRACEABILITY_KEYS).map(cloneTraceabilityKey),
119
118
  };
120
119
  }
121
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
+
122
125
  export function cloneValidationPolicy(policy = DEFAULT_VALIDATION_POLICY) {
123
126
  const filename = policy.markdown?.filename;
124
127
  return {
@@ -135,15 +138,11 @@ export function cloneValidationPolicy(policy = DEFAULT_VALIDATION_POLICY) {
135
138
 
136
139
  export function cloneImpactRankingPolicy(policy = DEFAULT_IMPACT_RANKING_POLICY) {
137
140
  return {
138
- preset: policy.preset ?? 'balanced',
139
- weights: { ...DEFAULT_IMPACT_RANKING_POLICY.weights, ...(policy.weights ?? {}) },
140
- ppr: { ...DEFAULT_IMPACT_RANKING_POLICY.ppr, ...(policy.ppr ?? {}) },
141
- relationWeights: { ...DEFAULT_IMPACT_RANKING_POLICY.relationWeights, ...(policy.relationWeights ?? {}) },
142
- traceabilityBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.traceabilityBoosts, ...(policy.traceabilityBoosts ?? {}) },
143
- verificationBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.verificationBoosts, ...(policy.verificationBoosts ?? {}) },
144
- semanticBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.semanticBoosts, ...(policy.semanticBoosts ?? {}) },
145
- proximityBoosts: { ...DEFAULT_IMPACT_RANKING_POLICY.proximityBoosts, ...(policy.proximityBoosts ?? {}) },
146
- 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 ?? {}) },
147
146
  };
148
147
  }
149
148
 
@@ -172,13 +171,16 @@ function normalizeReferenceExpansionPolicy(raw) {
172
171
  }
173
172
 
174
173
  function normalizeImpactRankingPolicy(raw) {
175
- const presetName = typeof raw?.preset === 'string' ? raw.preset : 'balanced';
176
- const preset = IMPACT_RANKING_PRESETS[presetName] ?? IMPACT_RANKING_PRESETS.balanced;
177
- return cloneImpactRankingPolicy({ ...preset, ...raw, preset: presetName, weights: { ...(preset.weights ?? {}), ...(raw?.weights ?? {}) }, ppr: { ...(preset.ppr ?? {}), ...(raw?.ppr ?? {}) }, semantic: { ...(preset.semantic ?? {}), ...(raw?.semantic ?? {}) } });
178
- }
179
-
180
- export function cloneIntegrationsPolicy(policy = DEFAULT_INTEGRATIONS_POLICY) {
181
- return { trello: { syncPaths: [...(policy.trello?.syncPaths ?? [])] } };
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 });
182
184
  }
183
185
 
184
186
  export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
@@ -196,7 +198,7 @@ export function clonePlanModePolicy(policy = DEFAULT_PLAN_MODE_POLICY) {
196
198
  }
197
199
 
198
200
  export function defaultMemoryConfig() {
199
- return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy(), load: cloneLoadPolicy(), planMode: clonePlanModePolicy() };
201
+ return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy(), planMode: clonePlanModePolicy() };
200
202
  }
201
203
 
202
204
  export function normalizePathPattern(value = '') {
@@ -244,6 +246,7 @@ function normalizeTraceabilityPolicy(raw) {
244
246
  return {
245
247
  required: Array.isArray(raw.required) ? raw.required.map(normalizePathPattern) : [],
246
248
  exclude: Array.isArray(raw.exclude) ? raw.exclude.map(normalizePathPattern) : [],
249
+ keys: (raw.keys === undefined ? DEFAULT_TRACEABILITY_KEYS : raw.keys).map(cloneTraceabilityKey),
247
250
  };
248
251
  }
249
252
 
@@ -264,12 +267,6 @@ function normalizeValidationPolicy(raw) {
264
267
  });
265
268
  }
266
269
 
267
- function normalizeIntegrationsPolicy(raw) {
268
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneIntegrationsPolicy();
269
- const syncPaths = Array.isArray(raw.trello?.syncPaths) ? raw.trello.syncPaths.map(normalizePathPattern) : [];
270
- return cloneIntegrationsPolicy({ trello: { syncPaths: [...new Set(syncPaths)] } });
271
- }
272
-
273
270
  export function isSecretLikePathPattern(value = '') {
274
271
  const normalized = normalizePathPattern(value);
275
272
  return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
@@ -287,8 +284,8 @@ function normalizeLoadPolicy(raw) {
287
284
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneLoadPolicy();
288
285
  const normalize = (values) => (Array.isArray(values) ? [...new Set(values.map(normalizePathPattern))] : []);
289
286
  return cloneLoadPolicy({
290
- pinnedPaths: normalize(raw.pinnedPaths),
291
- pinnedBodies: normalize(raw.pinnedBodies),
287
+ pinnedPaths: [],
288
+ pinnedBodies: [],
292
289
  documentationSummary: {
293
290
  exclude: raw.documentationSummary?.exclude === undefined
294
291
  ? DEFAULT_LOAD_POLICY.documentationSummary.exclude
@@ -297,10 +294,6 @@ function normalizeLoadPolicy(raw) {
297
294
  });
298
295
  }
299
296
 
300
- export function trelloSyncPaths(config = defaultMemoryConfig()) {
301
- return [...new Set(['docs/trello/**', ...((config.integrations ?? DEFAULT_INTEGRATIONS_POLICY).trello?.syncPaths ?? [])])];
302
- }
303
-
304
297
  export function isMarkdownSizeExcluded(path = '', config = defaultMemoryConfig()) {
305
298
  const policy = config.validation ?? DEFAULT_VALIDATION_POLICY;
306
299
  return (policy.markdown?.exclude ?? []).some((pattern) => matchPathPattern(path, pattern));
@@ -331,10 +324,6 @@ function isFiniteNumberInRange(value, min, max) {
331
324
  return typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
332
325
  }
333
326
 
334
- function validateNumberMap(map, keys, min, max) {
335
- return map && typeof map === 'object' && !Array.isArray(map) && Object.entries(map).every(([key, value]) => keys.has(key) && isFiniteNumberInRange(value, min, max));
336
- }
337
-
338
327
  export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.config.json') {
339
328
  const errors = [];
340
329
  const add = (code, field, message, fix = null) => errors.push({
@@ -355,6 +344,26 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
355
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.');
356
345
  if (traceability.exclude !== undefined && !Array.isArray(traceability.exclude)) add('TRACEABILITY_CONFIG_INVALID_EXCLUDE', 'traceability.exclude', 'Expected an array of path strings.');
357
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
+ }
358
367
  }
359
368
  }
360
369
  const validation = data.validation;
@@ -383,37 +392,11 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
383
392
  }
384
393
  }
385
394
  }
386
- const integrations = data.integrations;
387
- if (integrations !== undefined) {
388
- if (!integrations || typeof integrations !== 'object' || Array.isArray(integrations)) {
389
- add('INTEGRATIONS_CONFIG_INVALID', 'integrations', 'Expected an object.');
390
- } else if (integrations.trello !== undefined) {
391
- const trello = integrations.trello;
392
- if (!trello || typeof trello !== 'object' || Array.isArray(trello)) {
393
- add('INTEGRATIONS_TRELLO_CONFIG_INVALID', 'integrations.trello', 'Expected an object.');
394
- } else if (trello.syncPaths !== undefined) {
395
- if (!Array.isArray(trello.syncPaths)) add('INTEGRATIONS_TRELLO_INVALID_SYNC_PATHS', 'integrations.trello.syncPaths', 'Expected an array of path strings.');
396
- else {
397
- if (trello.syncPaths.some((value) => !isValidPathPattern(value) || (typeof value === 'string' && value.trim().startsWith('/')))) add('INTEGRATIONS_TRELLO_INVALID_SYNC_PATHS', 'integrations.trello.syncPaths', 'Expected repository-relative path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
398
- if (trello.syncPaths.some((value) => typeof value === 'string' && isSecretLikePathPattern(value))) add('INTEGRATIONS_TRELLO_SECRET_SYNC_PATH', 'integrations.trello.syncPaths', 'Sync paths must not target secrets, credentials, environment files, or private keys.');
399
- }
400
- }
401
- }
402
- }
403
395
  const load = data.load;
404
396
  if (load !== undefined) {
405
397
  if (!load || typeof load !== 'object' || Array.isArray(load)) {
406
398
  add('LOAD_CONFIG_INVALID', 'load', 'Expected an object.');
407
399
  } else {
408
- for (const [key, code] of [['pinnedPaths', 'LOAD_CONFIG_INVALID_PINNED_PATHS'], ['pinnedBodies', 'LOAD_CONFIG_INVALID_PINNED_BODIES']]) {
409
- const values = load[key];
410
- if (values === undefined) continue;
411
- if (!Array.isArray(values)) add(code, `load.${key}`, 'Expected an array of path strings.');
412
- else {
413
- 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.');
414
- 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.');
415
- }
416
- }
417
400
  if (load.documentationSummary !== undefined) {
418
401
  const summary = load.documentationSummary;
419
402
  if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
@@ -455,40 +438,6 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
455
438
  }
456
439
  }
457
440
  }
458
- const impactRanking = data.impactRanking;
459
- if (impactRanking !== undefined) {
460
- if (!impactRanking || typeof impactRanking !== 'object' || Array.isArray(impactRanking)) {
461
- add('IMPACT_RANKING_CONFIG_INVALID', 'impactRanking', 'Expected an object.');
462
- } else {
463
- 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.');
464
- 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.');
465
- 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.');
466
- for (const key of ['traceabilityBoosts', 'verificationBoosts', 'semanticBoosts', 'proximityBoosts']) {
467
- 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.');
468
- }
469
- if (impactRanking.ppr !== undefined) {
470
- const ppr = impactRanking.ppr;
471
- if (!ppr || typeof ppr !== 'object' || Array.isArray(ppr)) add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr', 'Expected an object.');
472
- else {
473
- if (ppr.enabled !== undefined && typeof ppr.enabled !== 'boolean') add('IMPACT_RANKING_CONFIG_INVALID_PPR', 'impactRanking.ppr.enabled', 'Expected a boolean.');
474
- 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.');
475
- 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.');
476
- 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.');
477
- }
478
- }
479
- if (impactRanking.semantic !== undefined) {
480
- const semantic = impactRanking.semantic;
481
- if (!semantic || typeof semantic !== 'object' || Array.isArray(semantic)) add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic', 'Expected an object.');
482
- else {
483
- if (semantic.enabled !== undefined && typeof semantic.enabled !== 'boolean') add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.enabled', 'Expected a boolean.');
484
- 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.');
485
- 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.');
486
- if (semantic.includeArchiveBodies !== undefined && typeof semantic.includeArchiveBodies !== 'boolean') add('IMPACT_RANKING_CONFIG_INVALID_SEMANTIC', 'impactRanking.semantic.includeArchiveBodies', 'Expected a boolean.');
487
- 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.');
488
- }
489
- }
490
- }
491
- }
492
441
  const areas = data.memory?.areas;
493
442
  if (areas === undefined) return errors;
494
443
  if (!Array.isArray(areas)) {
@@ -544,10 +493,9 @@ export function readMemoryConfig(root = '.') {
544
493
  const validation = data.validation === undefined ? cloneValidationPolicy() : normalizeValidationPolicy(data.validation);
545
494
  const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
546
495
  const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
547
- const integrations = normalizeIntegrationsPolicy(data.integrations);
548
496
  const load = normalizeLoadPolicy(data.load);
549
497
  const planMode = normalizePlanModePolicy(data.planMode);
550
- return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, source: MEMORY_CONFIG_FILE, errors: [] };
498
+ return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, load, planMode, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, load, planMode, source: MEMORY_CONFIG_FILE, errors: [] };
551
499
  } catch (error) {
552
500
  return { ...defaultMemoryConfig(), source: MEMORY_CONFIG_FILE, errors: [{ file: MEMORY_CONFIG_FILE, code: 'MEMORY_CONFIG_INVALID_JSON', message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}\nFix: repair ${MEMORY_CONFIG_FILE} so it is valid JSON, or remove it before regenerating the default config with \`dotdotgod config init <root>\`.` }] };
553
501
  }
@@ -575,9 +523,8 @@ export function defaultDotdotgodConfigData() {
575
523
  },
576
524
  traceability: cloneTraceabilityPolicy(config.traceability),
577
525
  validation: cloneValidationPolicy(config.validation),
578
- impactRanking: cloneImpactRankingPolicy(config.impactRanking),
526
+ impactRanking: { semantic: { ...config.impactRanking.semantic } },
579
527
  referenceExpansion: { fuzzy: { lowSignal: { add: [], remove: [] } } },
580
- integrations: cloneIntegrationsPolicy(config.integrations),
581
528
  load: cloneLoadPolicy(config.load),
582
529
  planMode: clonePlanModePolicy(config.planMode),
583
530
  };
@@ -603,9 +550,16 @@ export function memoryConfigSummary(config) {
603
550
  }, area)),
604
551
  traceability: cloneTraceabilityPolicy(config.traceability ?? DEFAULT_TRACEABILITY_POLICY),
605
552
  validation: cloneValidationPolicy(config.validation ?? DEFAULT_VALIDATION_POLICY),
606
- impactRanking: cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY),
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
+ },
607
562
  referenceExpansion: cloneReferenceExpansionPolicy(config.referenceExpansion),
608
- integrations: cloneIntegrationsPolicy(config.integrations),
609
563
  load: cloneLoadPolicy(config.load),
610
564
  planMode: clonePlanModePolicy(config.planMode),
611
565
  };
@@ -1,8 +1,6 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { rel } from '../common/paths.mjs';
4
- import { isExcludedIndexDir } from '../index/files.mjs';
5
- import { defaultMemoryConfig, isSecretLikePathPattern, matchPathPattern, memoryAreaForPath } from './config.mjs';
3
+ import { defaultMemoryConfig, memoryAreaForPath } from './config.mjs';
6
4
 
7
5
  export function detectPackageManager(root) {
8
6
  const packageFile = join(root, 'package.json');
@@ -50,98 +48,6 @@ export function detectCommandGuidance(root) {
50
48
  };
51
49
  }
52
50
 
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
-
145
51
  export function buildMemoryAreas(index, limits = {}) {
146
52
  const graph = index?.graph ?? { nodes: [], edges: [] };
147
53
  const itemLimit = limits.items ?? 4;
@@ -5,6 +5,20 @@ export const VECTOR_SCHEMA_VERSION = 1;
5
5
  export const VECTOR_MODEL = 'Xenova/multilingual-e5-small';
6
6
  export const VECTOR_DIMENSIONS = 384;
7
7
 
8
+ export function isValidNormalizedVectorValues(values) {
9
+ if (!values || values.length % VECTOR_DIMENSIONS !== 0) return false;
10
+ for (let offset = 0; offset < values.length; offset += VECTOR_DIMENSIONS) {
11
+ let squaredNorm = 0;
12
+ for (let index = 0; index < VECTOR_DIMENSIONS; index += 1) {
13
+ const value = values[offset + index];
14
+ if (!Number.isFinite(value) || value < -1 || value > 1) return false;
15
+ squaredNorm += value * value;
16
+ }
17
+ if (Math.abs(Math.sqrt(squaredNorm) - 1) > 0.01) return false;
18
+ }
19
+ return true;
20
+ }
21
+
8
22
  export function vectorCachePaths(root) {
9
23
  const directory = join(root, '.dotdotgod', 'vectors');
10
24
  return {
@@ -25,6 +39,7 @@ export function readVectorCache(root) {
25
39
  const buffer = readFileSync(paths.embeddings);
26
40
  if (buffer.byteLength !== chunks.length * VECTOR_DIMENSIONS * 4) return null;
27
41
  const vectors = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
42
+ if (!isValidNormalizedVectorValues(vectors)) return null;
28
43
  return { manifest, chunks, vectors: new Float32Array(vectors) };
29
44
  } catch {
30
45
  return null;
@@ -41,6 +56,7 @@ export function writeVectorCache(root, chunks, vectors, metadata = {}) {
41
56
  const paths = vectorCachePaths(root);
42
57
  mkdirSync(paths.directory, { recursive: true });
43
58
  if (vectors.length !== chunks.length * VECTOR_DIMENSIONS) throw new Error('Vector count does not match chunk metadata.');
59
+ if (!isValidNormalizedVectorValues(vectors)) throw new Error('Vectors must contain finite normalized values.');
44
60
  const manifest = {
45
61
  schemaVersion: VECTOR_SCHEMA_VERSION,
46
62
  model: VECTOR_MODEL,
@@ -61,9 +61,17 @@ export function runValidate(argv) {
61
61
  const maxChars = options.maxChars ?? validationPolicy.markdown.maxChars;
62
62
  const markdownSizePrompt = (path) => {
63
63
  const area = resolveMemoryArea(path, memoryConfig);
64
- const areaText = area ? `${area.label ?? area.id} (${area.id})` : 'the matching documentation area';
65
- const roleText = area?.role ?? 'the document role';
66
- return `Split ${path} into focused documents by documentation area and role. Keep the current ${areaText} area semantics and ${roleText} role intact, move role-specific detail into smaller UPPER_SNAKE_CASE markdown files, and update the nearest README.md index with each new file's purpose.`;
64
+ const clarify = area?.clarify;
65
+ const guidance = [
66
+ clarify?.documentType ? `document type: ${clarify.documentType}` : null,
67
+ clarify?.audience?.length ? `audience: ${clarify.audience.join(', ')}` : null,
68
+ clarify?.clarityGoal ? `clarity goal: ${clarify.clarityGoal}` : null,
69
+ clarify?.editRules?.length ? `edit rules: ${clarify.editRules.join('; ')}` : null,
70
+ ].filter(Boolean);
71
+ const evidence = guidance.length > 0
72
+ ? ` Follow this resolved clarification guidance: ${guidance.join('; ')}.`
73
+ : ' Preserve the document\'s current purpose and established meaning, using its content, links, and nearest README.md as evidence.';
74
+ return `Split ${path} into focused UPPER_SNAKE_CASE markdown files.${evidence} Keep each file focused and update the nearest README.md index with every new file's purpose.`;
67
75
  };
68
76
  for (const error of memoryConfig.errors ?? []) errors.push(error);
69
77
  const FILENAME_QUALITY_AREAS = new Set(
@@ -73,18 +81,14 @@ export function runValidate(argv) {
73
81
  .map(p => { const m = p.match(/^docs\/([^/*]+)\/\*\*$/); return m ? m[1] : null; })
74
82
  .filter(Boolean)
75
83
  );
84
+ const filenameQualityScope = [...FILENAME_QUALITY_AREAS].sort().map(area => `docs/${area}`).join(', ') || 'configured shared documentation areas';
76
85
  const FILENAME_QUALITY_PROMPT =
77
- 'Background: Filenames and paths are LLM/agent context signals used for impact ranking, search, and retrieval. ' +
78
- 'Abstract or sequentially numbered filenames carry low information signal.\n' +
79
- 'Naming principles:\n' +
80
- '1. Filenames in docs/spec, docs/arch, docs/test should be meaning-based wherever possible.\n' +
81
- '2. The filename alone should let you infer the primary subject: endpoint, screen, policy, state, or response type.\n' +
82
- '3. When splitting documents, derive names from headings, API paths, or domain terms rather than sequence numbers or abstract words.\n' +
83
- 'Avoid - numbered splits: API_1.md, API_2.md, API_3_1_*.md, 01_*.md, *_01.md\n' +
84
- 'Avoid - overly abstract: COMMON.md, OVERVIEW.md, DETAILS.md, NOTES.md, MISC.md, PART_1.md, SECTION_1.md\n' +
85
- 'Acceptable exceptions: README.md (directory index), files with clear HTTP status code semantics (e.g. RESPONSE_200_OK.md), ' +
86
- 'files in the project validation.markdown.filename.allow allowlist.\n' +
87
- 'Good renames: API_1.md → BIZ_RESERVATIONS_SCHEDULE.md, COMMON.md → BIZ_AUTH_ERROR_CONVENTIONS.md, OVERVIEW.md → REGISTRATION_HANDOFF_SUMMARY.md';
86
+ 'Filenames and paths are LLM/agent context signals used for impact ranking, search, and retrieval.\n' +
87
+ `Use meaning-based filenames in ${filenameQualityScope}. A filename should identify its primary endpoint, screen, policy, state, response type, or domain subject. ` +
88
+ 'Derive split-document names from headings, API paths, and established domain terms.\n' +
89
+ 'Rename numbered or abstract series such as API_1.md, PART_1.md, COMMON.md, or DETAILS.md to subject-specific names. ' +
90
+ 'Examples: API_1.md → BIZ_RESERVATIONS_SCHEDULE.md; COMMON.md → BIZ_AUTH_ERROR_CONVENTIONS.md.\n' +
91
+ 'README.md directory indexes, clear HTTP-status filenames such as RESPONSE_200_OK.md, and configured filename allowlist entries remain valid.';
88
92
  walk(docs);
89
93
  const dirSiblingMap = new Map();
90
94
  for (const f of markdownFiles) {
@@ -131,7 +135,7 @@ export function runValidate(argv) {
131
135
  if (block.error) addError(file, 'TRACEABILITY_INVALID_JSON', `Invalid \`json dotdotgod\` block: ${block.error}\nFix: repair the fenced \`json dotdotgod\` block so it is valid JSON and still matches the expected schema.\n\n${traceabilityExample()}`, block.line);
132
136
  else {
133
137
  for (const error of validateTraceabilityBlock(block.data, root, file, block.line, memoryConfig)) errors.push(error);
134
- const synced = syncTraceabilityLinksInContent(content, block.data, root, file);
138
+ const synced = syncTraceabilityLinksInContent(content, block.data, root, file, memoryConfig);
135
139
  if (synced.ok && synced.changed) addError(file, 'TRACEABILITY_LINKS_STALE', 'Generated traceability links or compact traceability JSON are missing or stale.', block.line, 'run `dotdotgod traceability links <root> --write` to synchronize the generated Markdown links and compact JSON block.');
136
140
  }
137
141
  }