@dotdotgod/cli 0.1.29 → 0.1.35

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
@@ -31,6 +31,7 @@ Command-line tools for dotdotgod project memory. The CLI turns explicit, maintai
31
31
 
32
32
  - Initialize `AGENTS.md`, thin agent entrypoints, docs indexes, active-plan space, archive map, and local cache ignores with `dotdotgod init`.
33
33
  - Replace ad-hoc docs checks with `dotdotgod validate`.
34
+ - Keep generated Markdown traceability links synchronized with `dotdotgod traceability links`.
34
35
  - Build `.dotdotgod/` as a local ignored cache of file fingerprints and compact graph shards derived from maintained project links.
35
36
  - Use `load-snapshot` as a bounded first-pass map for high-quality agent loading.
36
37
  - Resolve explicit `[[...]]` references or high-signal fuzzy references before broad text search.
@@ -53,13 +54,26 @@ dotdotgod load-snapshot .
53
54
  dotdotgod resolve . PLAN_MODE
54
55
  dotdotgod expand . "Update [[PLAN_MODE]] and [[HOOKS]]"
55
56
  dotdotgod expand . "PLAN_MODE 수정하자" --fuzzy
57
+ dotdotgod traceability links . --check
58
+ dotdotgod traceability links . --write
56
59
  dotdotgod graph impact . --changed <path>
57
60
  dotdotgod graph impact . --changed <path> --compact
58
61
  dotdotgod graph impact . --changed <path> --yml
59
62
  dotdotgod graph communities .
60
63
  ```
61
64
 
62
- `--help`, `-h`, and `help` print usage to stdout. Command-specific help is available with `dotdotgod <command> --help`, including nested commands such as `dotdotgod graph impact --help` and `dotdotgod config init --help`.
65
+ `--help`, `-h`, and `help` print usage to stdout. Command-specific help is available with `dotdotgod <command> --help`, including nested commands such as `dotdotgod graph impact --help`, `dotdotgod config init --help`, and `dotdotgod traceability links --help`.
66
+
67
+ ## Validation and Verification Boundaries
68
+
69
+ | Command | Responsibility |
70
+ | --- | --- |
71
+ | `dotdotgod validate <root>` | Checks dotdotgod docs/project-memory structure, local links, traceability blocks, generated traceability-link drift, config validity, and optional index freshness. |
72
+ | `dotdotgod traceability links <root> --check` | Runs only the generated traceability-link and compact JSON drift check. |
73
+ | `dotdotgod traceability links <root> --write` | Repairs generated traceability-link sections and rewrites canonical `json dotdotgod` blocks as compact JSON. |
74
+ | `pnpm run verify` | Runs the repository-level quality gate: generated-resource checks, package verify contracts, tests, typecheck, and docs validation where each package defines it. |
75
+
76
+ Most users start with `validate`. Use `traceability links --write` only when generated traceability output needs repair, and use `verify` before release-style handoff.
63
77
 
64
78
  ## Graph, Cache, and Impact
65
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.35",
4
4
  "description": "CLI for dotdotgod validation, project memory snapshots, and graph indexing.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
package/src/cli/usage.mjs CHANGED
@@ -40,6 +40,14 @@ Create dotdotgod.config.json with the built-in default memory, traceability, val
40
40
  case 'expand':
41
41
  return `Usage:
42
42
  dotdotgod expand <root> <prompt> [--max-results n] [--include-archive] [--with-impact] [--fuzzy] [--json]`;
43
+ case 'traceability':
44
+ return `Usage:
45
+ dotdotgod traceability links <root> [--check|--write] [--json]`;
46
+ case 'traceability links':
47
+ return `Usage:
48
+ dotdotgod traceability links <root> [--check|--write] [--json]
49
+
50
+ Checks or writes generated Markdown traceability link sections from canonical fenced json dotdotgod blocks.`;
43
51
  case 'graph':
44
52
  return `Usage:
45
53
  dotdotgod graph impact <root> --changed <path> [--compact|--json|--yml|--yaml]
@@ -66,6 +74,7 @@ Ranks nodes related to a changed file. <root> is the project root; --changed is
66
74
  dotdotgod load-snapshot <root> [--json]
67
75
  dotdotgod resolve <root> <ref> [--max-results n] [--include-archive] [--json]
68
76
  dotdotgod expand <root> <prompt> [--max-results n] [--include-archive] [--with-impact] [--fuzzy] [--json]
77
+ dotdotgod traceability links <root> [--check|--write] [--json]
69
78
  dotdotgod graph impact <root> --changed <path> [--compact|--json|--yml|--yaml]
70
79
  dotdotgod graph communities <root> [--json]`;
71
80
  }
@@ -111,6 +120,7 @@ export function printVersion() {
111
120
  export function helpCommandFromArgs(args) {
112
121
  const nonHelp = args.filter((arg) => !isHelpToken(arg));
113
122
  if (nonHelp[0] === 'graph' && nonHelp[1]) return `graph ${nonHelp[1]}`;
123
+ if (nonHelp[0] === 'traceability' && nonHelp[1]) return `traceability ${nonHelp[1]}`;
114
124
  if (nonHelp[0] === 'config' && nonHelp[1] === 'init') return 'config init';
115
125
  return nonHelp[0] ?? 'root';
116
126
  }
package/src/core.mjs CHANGED
@@ -6,7 +6,7 @@ import { runInit } from './init.mjs';
6
6
  import { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersionToken, parseCommon, printVersion, usage } from './cli/usage.mjs';
7
7
  import { rel } from './common/paths.mjs';
8
8
  import { extractAnchors, extractLinks, headingToAnchor, isKebabCase, isUpperSnakeMarkdown, removeCodeBlocks } from './docs/markdown.mjs';
9
- import { extractDotdotgodTraceabilityBlocks, isLocalRelativeTraceabilityPath, traceabilityExample, validateTraceabilityBlock, validateTraceabilityPlacement } from './docs/traceability.mjs';
9
+ import { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
10
10
  import { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, memoryAreaForPath, memoryConfigSummary, memoryRoleForPath, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, retrievalPriorityForPath, validateMemoryConfigData } from './memory/config.mjs';
11
11
  import { buildCommunities, relationWeight } from './graph/communities.mjs';
12
12
  import { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
@@ -16,13 +16,13 @@ import { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from '.
16
16
  export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersionToken, parseCommon, printVersion, usage } from './cli/usage.mjs';
17
17
  export { rel } from './common/paths.mjs';
18
18
  export { extractAnchors, extractLinks, headingToAnchor, isKebabCase, isUpperSnakeMarkdown, removeCodeBlocks } from './docs/markdown.mjs';
19
- export { extractDotdotgodTraceabilityBlocks, isLocalRelativeTraceabilityPath, traceabilityExample, validateTraceabilityBlock, validateTraceabilityPlacement } from './docs/traceability.mjs';
19
+ export { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
20
20
  export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, memoryAreaForPath, memoryConfigSummary, memoryRoleForPath, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, retrievalPriorityForPath, validateMemoryConfigData } from './memory/config.mjs';
21
21
  export { buildCommunities, relationWeight } from './graph/communities.mjs';
22
22
  export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
23
23
  export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
24
24
  export { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from './load-snapshot/summary.mjs';
25
- export const CACHE_VERSION = 10;
25
+ export const CACHE_VERSION = 11;
26
26
  const CACHE_DIR = '.dotdotgod';
27
27
  const MANIFEST_FILE = 'manifest.json';
28
28
  export function runValidate(argv) {
@@ -81,18 +81,25 @@ export function runValidate(argv) {
81
81
  if (docsRel && !docsRel.startsWith('..') && !isUpperSnakeMarkdown(name)) addError(file, 'FILE_NAMING', `Markdown file must be UPPER_SNAKE_CASE.md or README.md: ${name}`, null, 'rename the markdown file to UPPER_SNAKE_CASE.md or README.md and update any links that reference it.');
82
82
  const content = readFileSync(file, 'utf8');
83
83
  fileCache.set(file, content);
84
- const lines = content.split('\n').length;
84
+ const budgetContent = stripTraceabilityLinksRegion(content);
85
+ const lines = budgetContent.split('\n').length;
86
+ const chars = budgetContent.length;
85
87
  const path = rel(root, file);
86
88
  const skipSizeChecks = isMarkdownSizeExcluded(path, memoryConfig);
87
- if (!skipSizeChecks && lines > maxLines) addError(file, 'FILE_TOO_LONG', `Markdown file has ${lines} lines; max is ${maxLines}`, null, 'split the document into focused markdown files and update the nearest README.md index, or add a narrow validation.markdown.exclude entry if this file is intentionally oversized.');
88
- if (!skipSizeChecks && content.length > maxChars) addError(file, 'FILE_TOO_LARGE', `Markdown file has ${content.length} characters; max is ${maxChars}`, null, 'split the document into focused markdown files and update the nearest README.md index, or add a narrow validation.markdown.exclude entry if this file is intentionally oversized.');
89
+ if (!skipSizeChecks && lines > maxLines) addError(file, 'FILE_TOO_LONG', `Markdown file has ${lines} lines excluding generated traceability links and traceability JSON blocks; max is ${maxLines}`, null, 'split the document into focused markdown files and update the nearest README.md index, or add a narrow validation.markdown.exclude entry if this file is intentionally oversized.');
90
+ if (!skipSizeChecks && chars > maxChars) addError(file, 'FILE_TOO_LARGE', `Markdown file has ${chars} characters excluding generated traceability links and traceability JSON blocks; max is ${maxChars}`, null, 'split the document into focused markdown files and update the nearest README.md index, or add a narrow validation.markdown.exclude entry if this file is intentionally oversized.');
89
91
  if (requiresTraceability(rel(root, file), memoryConfig)) {
90
92
  const blocks = extractDotdotgodTraceabilityBlocks(content);
91
93
  if (blocks.length === 0) addError(file, 'TRACEABILITY_MISSING', `Behavior specs must include a fenced \`json dotdotgod\` traceability block as the final section.\nFix: add a final \`## Traceability\` section with the expected \`json dotdotgod\` block and point it at the relevant source, tests, related docs, and verification commands.\n\n${traceabilityExample()}`);
92
94
  else for (const error of validateTraceabilityPlacement(content, root, file)) errors.push(error);
95
+ for (const error of validateTraceabilityLinksRegion(content, root, file)) errors.push(error);
93
96
  for (const block of blocks) {
94
97
  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);
95
- else for (const error of validateTraceabilityBlock(block.data, root, file, block.line)) errors.push(error);
98
+ else {
99
+ for (const error of validateTraceabilityBlock(block.data, root, file, block.line, memoryConfig)) errors.push(error);
100
+ const synced = syncTraceabilityLinksInContent(content, block.data, root, file);
101
+ 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.');
102
+ }
96
103
  }
97
104
  }
98
105
  }
@@ -1368,6 +1375,75 @@ export function runExpand(argv) {
1368
1375
  else console.log(formatReferenceOutput(payload));
1369
1376
  }
1370
1377
 
1378
+ function collectDocsMarkdownFiles(root) {
1379
+ const docs = join(root, 'docs');
1380
+ const files = [];
1381
+ const walk = (dir) => {
1382
+ if (!existsSync(dir)) return;
1383
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1384
+ const path = join(dir, entry.name);
1385
+ if (entry.isDirectory()) walk(path);
1386
+ else if (entry.isFile() && entry.name.endsWith('.md')) files.push(path);
1387
+ }
1388
+ };
1389
+ walk(docs);
1390
+ return files;
1391
+ }
1392
+
1393
+ export function parseTraceabilityOptions(argv) {
1394
+ const sub = argv[0];
1395
+ const action = argv[1];
1396
+ if (sub !== 'links') usage(sub ? `Unknown traceability command: ${sub}` : 'Missing traceability command.', 'traceability');
1397
+ const options = { root: '.', check: false, write: false, json: false };
1398
+ for (let i = 1; i < argv.length; i += 1) {
1399
+ const arg = argv[i];
1400
+ if (arg === '--check') options.check = true;
1401
+ else if (arg === '--write') options.write = true;
1402
+ else if (arg === '--json') options.json = true;
1403
+ else if (!arg.startsWith('-')) options.root = arg;
1404
+ else usage(`Unknown option: ${arg}`, 'traceability links');
1405
+ }
1406
+ if (action === '--check' || action === '--write' || action === '--json') {
1407
+ // no-op: action slot is an option, not a positional subcommand.
1408
+ }
1409
+ if (options.check && options.write) usage('Choose only one traceability links mode: --check or --write.', 'traceability links');
1410
+ options.check = options.check || !options.write;
1411
+ options.root = resolve(options.root);
1412
+ return options;
1413
+ }
1414
+
1415
+ export function runTraceability(argv) {
1416
+ const options = parseTraceabilityOptions(argv);
1417
+ const files = collectDocsMarkdownFiles(options.root);
1418
+ const results = [];
1419
+ const errors = [];
1420
+ for (const file of files) {
1421
+ const content = readFileSync(file, 'utf8');
1422
+ for (const error of validateTraceabilityLinksRegion(content, options.root, file)) errors.push(error);
1423
+ const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
1424
+ if (blocks.length === 0) continue;
1425
+ const result = syncTraceabilityLinksInContent(content, blocks.at(-1).data, options.root, file);
1426
+ if (!result.ok) {
1427
+ for (const error of result.errors ?? []) errors.push(error);
1428
+ continue;
1429
+ }
1430
+ if (options.write && result.changed) writeFileSync(file, result.content);
1431
+ if (result.changed) results.push({ file: rel(options.root, file), changed: true });
1432
+ }
1433
+ const ok = errors.length === 0 && (!options.check || results.length === 0);
1434
+ const payload = { ok, command: 'traceability links', root: options.root, mode: options.write ? 'write' : 'check', changed: results.length, files: results, errors };
1435
+ if (options.json) console.log(JSON.stringify(payload, null, 2));
1436
+ else if (errors.length > 0) {
1437
+ for (const error of errors) console.log(`${error.line ? `${error.file}:${error.line}` : error.file} [${error.code}] ${error.message}`);
1438
+ console.log(`\n❌ ${errors.length} traceability link error(s)`);
1439
+ } else if (options.check && results.length > 0) {
1440
+ for (const result of results) console.log(`${result.file} [TRACEABILITY_LINKS_STALE] generated traceability links are missing or stale.`);
1441
+ console.log(`\n❌ ${results.length} traceability link file(s) need sync`);
1442
+ } else if (options.write) console.log(`traceability links: synced ${results.length} file(s)`);
1443
+ else console.log('traceability links: all generated sections are up to date');
1444
+ process.exit(ok ? 0 : 1);
1445
+ }
1446
+
1371
1447
  export function parseGraphOptions(argv) {
1372
1448
  const filtered = [];
1373
1449
  let changed;
@@ -1443,6 +1519,7 @@ export function runCli(argv = process.argv.slice(2)) {
1443
1519
  else if (command === 'load-snapshot') runLoadSnapshot(args);
1444
1520
  else if (command === 'resolve') runResolve(args);
1445
1521
  else if (command === 'expand') runExpand(args);
1522
+ else if (command === 'traceability') runTraceability(args);
1446
1523
  else if (command === 'graph') runGraph(args);
1447
1524
  else usage(`Unknown command: ${command}`);
1448
1525
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
2
+ import { basename, dirname, extname, relative, resolve } from 'node:path';
3
3
  import { rel } from '../common/paths.mjs';
4
+ import { defaultMemoryConfig, resolveMemoryArea } from '../memory/config.mjs';
4
5
 
5
6
  function isSecretIndexPath(path) {
6
7
  return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials|secrets?)(\.|\/|$)/i.test(path);
@@ -8,6 +9,8 @@ function isSecretIndexPath(path) {
8
9
 
9
10
  const TRACEABILITY_PATH_FIELDS = ['implementedBy', 'verifiedBy', 'relatedDocs'];
10
11
  const TRACEABILITY_COMMAND_FIELDS = ['verificationCommands'];
12
+ export const TRACEABILITY_LINKS_START = '<!-- dotdotgod:traceability-links:start version=1 source=json-dotdotgod -->';
13
+ export const TRACEABILITY_LINKS_END = '<!-- dotdotgod:traceability-links:end -->';
11
14
 
12
15
  export function traceabilityExample() {
13
16
  return 'Expected dotdotgod traceability block:\n\n```json dotdotgod\n{\n "kind": "spec",\n "implementedBy": ["packages/..."],\n "verifiedBy": ["packages/..."],\n "relatedDocs": ["docs/..."],\n "verificationCommands": ["pnpm ..."]\n}\n```\n\nProperty guidance:\n- kind: use "spec" for behavior specs.\n- implementedBy: source/config/script files that implement this spec\'s behavior.\n- verifiedBy: test files or verification docs that check this behavior.\n- relatedDocs: docs with relevant architecture, test strategy, or product context.\n- verificationCommands: commands an agent can run to verify this behavior.';
@@ -27,9 +30,9 @@ export function extractDotdotgodTraceabilityBlocks(content) {
27
30
  const raw = match[3].trim();
28
31
  const line = lineForOffset(content, match.index);
29
32
  try {
30
- blocks.push({ data: JSON.parse(raw), raw, line });
33
+ blocks.push({ data: JSON.parse(raw), raw, line, index: match.index, end: re.lastIndex });
31
34
  } catch (error) {
32
- blocks.push({ error: error instanceof Error ? error.message : String(error), raw, line });
35
+ blocks.push({ error: error instanceof Error ? error.message : String(error), raw, line, index: match.index, end: re.lastIndex });
33
36
  }
34
37
  }
35
38
  return blocks;
@@ -55,7 +58,117 @@ export function validateTraceabilityPlacement(content, root, file) {
55
58
  return [];
56
59
  }
57
60
 
58
- export function validateTraceabilityBlock(data, root, file, line = null) {
61
+ function allMarkerIndexes(content, marker) {
62
+ const indexes = [];
63
+ let index = content.indexOf(marker);
64
+ while (index !== -1) {
65
+ indexes.push(index);
66
+ index = content.indexOf(marker, index + marker.length);
67
+ }
68
+ return indexes;
69
+ }
70
+
71
+ export function findTraceabilityLinksRegion(content) {
72
+ const starts = allMarkerIndexes(content, TRACEABILITY_LINKS_START);
73
+ const ends = allMarkerIndexes(content, TRACEABILITY_LINKS_END);
74
+ if (starts.length === 0 && ends.length === 0) return { status: 'missing' };
75
+ if (starts.length !== 1 || ends.length !== 1) return { status: 'invalid', code: 'TRACEABILITY_LINKS_MARKER_COUNT', message: 'Traceability links region must contain exactly one start marker and one end marker.' };
76
+ const start = starts[0];
77
+ const end = ends[0];
78
+ if (start > end) return { status: 'invalid', code: 'TRACEABILITY_LINKS_MARKER_ORDER', message: 'Traceability links start marker must appear before the end marker.' };
79
+ return { status: 'present', start, end: end + TRACEABILITY_LINKS_END.length };
80
+ }
81
+
82
+ export function validateTraceabilityLinksRegion(content, root, file) {
83
+ const region = findTraceabilityLinksRegion(content);
84
+ if (region.status !== 'invalid') return [];
85
+ return [traceabilityFieldError(rel(root, file), region.code, null, region.message)];
86
+ }
87
+
88
+ export function stripTraceabilityLinksRegion(content) {
89
+ const ranges = [];
90
+ const region = findTraceabilityLinksRegion(content);
91
+ if (region.status === 'present') ranges.push({ start: region.start, end: region.end });
92
+ for (const block of extractDotdotgodTraceabilityBlocks(content)) ranges.push({ start: block.index, end: block.end });
93
+ if (ranges.length === 0) return content;
94
+ return ranges
95
+ .sort((a, b) => b.start - a.start)
96
+ .reduce((next, range) => `${next.slice(0, range.start)}${next.slice(range.end)}`, content);
97
+ }
98
+
99
+ function markdownLinkPath(root, file, target) {
100
+ const fromDir = dirname(file);
101
+ const absoluteTarget = resolve(root, target);
102
+ let link = relative(fromDir, absoluteTarget).split('\\').join('/');
103
+ if (!link.startsWith('.') && !link.startsWith('/')) link = link || basename(target);
104
+ return link;
105
+ }
106
+
107
+ function escapeMarkdownText(value) {
108
+ return String(value).replace(/([\\[\\]])/g, '\\$1');
109
+ }
110
+
111
+ function escapeCodeSpan(value) {
112
+ return String(value).replace(/`/g, '\\`');
113
+ }
114
+
115
+ function labelForTraceabilityPath(path) {
116
+ return path;
117
+ }
118
+
119
+ function renderPathList(title, paths, root, file) {
120
+ if (!Array.isArray(paths) || paths.length === 0) return [];
121
+ const lines = [`- ${title}:`];
122
+ for (const target of paths) lines.push(` - [${escapeMarkdownText(labelForTraceabilityPath(target))}](${markdownLinkPath(root, file, target)})`);
123
+ return lines;
124
+ }
125
+
126
+ function renderCommandList(commands) {
127
+ if (!Array.isArray(commands) || commands.length === 0) return [];
128
+ return ['- Verification commands:', ...commands.map((command) => ` - \`${escapeCodeSpan(command)}\``)];
129
+ }
130
+
131
+ export function renderTraceabilityLinks(data, root, file) {
132
+ const body = [
133
+ '### Traceability Links',
134
+ '',
135
+ ...renderPathList('Implemented by', data?.implementedBy, root, file),
136
+ ...renderPathList('Verified by', data?.verifiedBy, root, file),
137
+ ...renderPathList('Related docs', data?.relatedDocs, root, file),
138
+ ...renderCommandList(data?.verificationCommands),
139
+ ];
140
+ return `${TRACEABILITY_LINKS_START}\n<!-- generated: do not edit manually -->\n\n${body.join('\n')}\n\n${TRACEABILITY_LINKS_END}`;
141
+ }
142
+
143
+ export function renderCompactTraceabilityBlock(data) {
144
+ return `\`\`\`json dotdotgod\n${JSON.stringify(data)}\n\`\`\``;
145
+ }
146
+
147
+ export function syncTraceabilityLinksInContent(content, data, root, file) {
148
+ const region = findTraceabilityLinksRegion(content);
149
+ if (region.status === 'invalid') return { ok: false, changed: false, errors: validateTraceabilityLinksRegion(content, root, file) };
150
+ const generated = renderTraceabilityLinks(data, root, file);
151
+ const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
152
+ if (blocks.length === 0) return { ok: false, changed: false, errors: [traceabilityFieldError(rel(root, file), 'TRACEABILITY_LINKS_MISSING_BLOCK', null, 'Cannot generate traceability links without a valid `json dotdotgod` block.')] };
153
+ const block = blocks.at(-1);
154
+ const compactBlock = renderCompactTraceabilityBlock(data);
155
+ let next = `${content.slice(0, block.index)}${compactBlock}${content.slice(block.end)}`;
156
+ const adjustedRegion = region.status === 'present' && region.start > block.index
157
+ ? { start: region.start - (block.end - block.index) + compactBlock.length, end: region.end - (block.end - block.index) + compactBlock.length }
158
+ : region;
159
+ if (adjustedRegion.status === 'present') {
160
+ next = `${next.slice(0, adjustedRegion.start)}${generated}${next.slice(adjustedRegion.end)}`;
161
+ return { ok: true, changed: next !== content, content: next };
162
+ }
163
+ const refreshedBlocks = extractDotdotgodTraceabilityBlocks(next).filter((item) => !item.error);
164
+ const refreshedBlock = refreshedBlocks.at(-1);
165
+ const prefix = next.slice(0, refreshedBlock.index).replace(/[ \t]*$/u, '');
166
+ const suffix = next.slice(refreshedBlock.index);
167
+ next = `${prefix}\n\n${generated}\n\n${suffix}`;
168
+ return { ok: true, changed: next !== content, content: next };
169
+ }
170
+
171
+ export function validateTraceabilityBlock(data, root, file, line = null, config = defaultMemoryConfig()) {
59
172
  const errors = [];
60
173
  const add = (code, field, message) => errors.push(traceabilityFieldError(rel(root, file), code, field, message, line));
61
174
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
@@ -73,6 +186,11 @@ export function validateTraceabilityBlock(data, root, file, line = null) {
73
186
  add('TRACEABILITY_INVALID_PATH', field, `invalid local relative path: ${JSON.stringify(value)}.`);
74
187
  continue;
75
188
  }
189
+ const area = resolveMemoryArea(value, config);
190
+ if (area?.scope === 'local') {
191
+ add('TRACEABILITY_LOCAL_MEMORY_TARGET', field, `target points to local memory area "${area.id}": ${value}. Traceability must point to shared durable files, not active plans or archive memory.`);
192
+ continue;
193
+ }
76
194
  if (!existsSync(resolve(root, value))) add('TRACEABILITY_MISSING_TARGET', field, `target does not exist: ${value}.`);
77
195
  }
78
196
  }
@@ -54,7 +54,20 @@ export function buildMemoryAreas(index, limits = {}) {
54
54
  const config = index?.memoryConfig?.areas ? index.memoryConfig : defaultMemoryConfig();
55
55
  const areas = new Map();
56
56
  for (const definition of config.areas ?? []) {
57
- areas.set(definition.id, { area: definition.id, label: definition.label, role: definition.role, scope: definition.scope, freshness: definition.freshness, priority: definition.priority, includeBodiesByDefault: definition.includeBodiesByDefault !== false, files: [], count: 0, omitted: 0 });
57
+ areas.set(definition.id, {
58
+ area: definition.id,
59
+ label: definition.label,
60
+ role: definition.role,
61
+ ...(definition.description ? { description: definition.description } : {}),
62
+ ...(definition.clarify ? { clarify: definition.clarify } : {}),
63
+ scope: definition.scope,
64
+ freshness: definition.freshness,
65
+ priority: definition.priority,
66
+ includeBodiesByDefault: definition.includeBodiesByDefault !== false,
67
+ files: [],
68
+ count: 0,
69
+ omitted: 0,
70
+ });
58
71
  }
59
72
  for (const node of graph.nodes) {
60
73
  if (node.type !== 'file') continue;
@@ -62,12 +62,32 @@ const DEFAULT_MEMORY_AREAS = [
62
62
  { id: 'archive-body', label: 'Archive Body', paths: ['docs/archive/**'], excludePaths: ['docs/archive/README.md'], scope: 'local', freshness: 'stale', role: 'historical-memory-body', priority: 20, includeBodiesByDefault: false },
63
63
  ];
64
64
 
65
- function cloneArea(area) {
65
+ function cloneClarifyGuidance(clarify) {
66
+ if (!clarify || typeof clarify !== 'object' || Array.isArray(clarify)) return undefined;
67
+ return {
68
+ ...(Array.isArray(clarify.audience) ? { audience: [...clarify.audience] } : {}),
69
+ ...(typeof clarify.documentType === 'string' ? { documentType: clarify.documentType } : {}),
70
+ ...(typeof clarify.clarityGoal === 'string' ? { clarityGoal: clarify.clarityGoal } : {}),
71
+ ...(Array.isArray(clarify.editRules) ? { editRules: [...clarify.editRules] } : {}),
72
+ };
73
+ }
74
+
75
+ function withOptionalAreaMetadata(target, area) {
76
+ const description = typeof area.description === 'string' ? area.description.trim() : '';
77
+ const clarify = cloneClarifyGuidance(area.clarify);
66
78
  return {
79
+ ...target,
80
+ ...(description ? { description } : {}),
81
+ ...(clarify && Object.keys(clarify).length > 0 ? { clarify } : {}),
82
+ };
83
+ }
84
+
85
+ function cloneArea(area) {
86
+ return withOptionalAreaMetadata({
67
87
  ...area,
68
88
  paths: [...(area.paths ?? [])],
69
89
  excludePaths: [...(area.excludePaths ?? [])],
70
- };
90
+ }, area);
71
91
  }
72
92
 
73
93
  export function cloneTraceabilityPolicy(policy = DEFAULT_TRACEABILITY_POLICY) {
@@ -216,7 +236,7 @@ export function requiresTraceability(path = '', config = defaultMemoryConfig())
216
236
  }
217
237
 
218
238
  function normalizeMemoryArea(raw) {
219
- return {
239
+ return withOptionalAreaMetadata({
220
240
  id: raw.id,
221
241
  label: raw.label ?? raw.id,
222
242
  paths: Array.isArray(raw.paths) ? raw.paths.map(normalizePathPattern) : [],
@@ -226,7 +246,7 @@ function normalizeMemoryArea(raw) {
226
246
  role: raw.role ?? raw.id,
227
247
  priority: typeof raw.priority === 'number' ? raw.priority : 30,
228
248
  includeBodiesByDefault: raw.includeBodiesByDefault !== false,
229
- };
249
+ }, raw);
230
250
  }
231
251
 
232
252
  function isFiniteNumberInRange(value, min, max) {
@@ -352,6 +372,18 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
352
372
  if (!MEMORY_FRESHNESS.has(area.freshness)) add('MEMORY_CONFIG_INVALID_FRESHNESS', `${prefix}.freshness`, 'Expected "fresh" or "stale".');
353
373
  if (area.priority !== undefined && (!Number.isInteger(area.priority) || area.priority < 0 || area.priority > 100)) add('MEMORY_CONFIG_INVALID_PRIORITY', `${prefix}.priority`, 'Expected an integer from 0 to 100.');
354
374
  if (area.includeBodiesByDefault !== undefined && typeof area.includeBodiesByDefault !== 'boolean') add('MEMORY_CONFIG_INVALID_INCLUDE_POLICY', `${prefix}.includeBodiesByDefault`, 'Expected a boolean.');
375
+ if (area.description !== undefined && (typeof area.description !== 'string' || !area.description.trim())) add('MEMORY_CONFIG_INVALID_DESCRIPTION', `${prefix}.description`, 'Expected a non-empty string.');
376
+ if (area.clarify !== undefined) {
377
+ const clarify = area.clarify;
378
+ if (!clarify || typeof clarify !== 'object' || Array.isArray(clarify)) {
379
+ add('MEMORY_CONFIG_INVALID_CLARIFY', `${prefix}.clarify`, 'Expected an object.');
380
+ } else {
381
+ if (clarify.audience !== undefined && (!Array.isArray(clarify.audience) || clarify.audience.some((value) => typeof value !== 'string' || !value.trim()))) add('MEMORY_CONFIG_INVALID_CLARIFY_AUDIENCE', `${prefix}.clarify.audience`, 'Expected an array of non-empty strings.');
382
+ if (clarify.documentType !== undefined && (typeof clarify.documentType !== 'string' || !clarify.documentType.trim())) add('MEMORY_CONFIG_INVALID_CLARIFY_DOCUMENT_TYPE', `${prefix}.clarify.documentType`, 'Expected a non-empty string.');
383
+ if (clarify.clarityGoal !== undefined && (typeof clarify.clarityGoal !== 'string' || !clarify.clarityGoal.trim())) add('MEMORY_CONFIG_INVALID_CLARITY_GOAL', `${prefix}.clarify.clarityGoal`, 'Expected a non-empty string.');
384
+ if (clarify.editRules !== undefined && (!Array.isArray(clarify.editRules) || clarify.editRules.some((value) => typeof value !== 'string' || !value.trim()))) add('MEMORY_CONFIG_INVALID_CLARIFY_EDIT_RULES', `${prefix}.clarify.editRules`, 'Expected an array of non-empty strings.');
385
+ }
386
+ }
355
387
  for (const pattern of area.paths ?? []) {
356
388
  if (exactIncluded.has(pattern) && !(area.excludePaths ?? []).includes(pattern)) add('MEMORY_CONFIG_OVERLAP', `${prefix}.paths`, `Path pattern also appears in ${exactIncluded.get(pattern)}: ${pattern}`);
357
389
  else exactIncluded.set(pattern, `${prefix}.paths`);
@@ -382,7 +414,7 @@ export function readMemoryConfig(root = '.') {
382
414
  }
383
415
 
384
416
  function serializableMemoryArea(area) {
385
- return {
417
+ return withOptionalAreaMetadata({
386
418
  id: area.id,
387
419
  label: area.label,
388
420
  paths: [...(area.paths ?? [])],
@@ -392,7 +424,7 @@ function serializableMemoryArea(area) {
392
424
  role: area.role,
393
425
  priority: area.priority,
394
426
  includeBodiesByDefault: area.includeBodiesByDefault !== false,
395
- };
427
+ }, area);
396
428
  }
397
429
 
398
430
  export function defaultDotdotgodConfigData() {
@@ -415,17 +447,17 @@ export function defaultDotdotgodConfigText() {
415
447
  export function memoryConfigSummary(config) {
416
448
  return {
417
449
  source: config.source ?? 'default',
418
- areas: (config.areas ?? []).map((area) => ({
450
+ areas: (config.areas ?? []).map((area) => withOptionalAreaMetadata({
419
451
  id: area.id,
420
452
  label: area.label,
421
- paths: area.paths,
422
- excludePaths: area.excludePaths ?? [],
453
+ paths: [...(area.paths ?? [])],
454
+ excludePaths: [...(area.excludePaths ?? [])],
423
455
  scope: area.scope,
424
456
  freshness: area.freshness,
425
457
  role: area.role,
426
458
  priority: area.priority,
427
459
  includeBodiesByDefault: area.includeBodiesByDefault !== false,
428
- })),
460
+ }, area)),
429
461
  traceability: cloneTraceabilityPolicy(config.traceability ?? DEFAULT_TRACEABILITY_POLICY),
430
462
  validation: cloneValidationPolicy(config.validation ?? DEFAULT_VALIDATION_POLICY),
431
463
  impactRanking: cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY),