@dotdotgod/claude-code 0.3.9 → 0.3.11

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/mcp/cli.mjs CHANGED
@@ -1345,12 +1345,18 @@ Checks or writes generated Markdown traceability link sections from canonical fe
1345
1345
  case "graph":
1346
1346
  return `Usage:
1347
1347
  dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
1348
+ dotdotgod graph serve <root> --changed <path> [--changed <path> ...] [--host HOST] [--port PORT]
1348
1349
  dotdotgod graph communities <root> [--json]`;
1349
1350
  case "graph impact":
1350
1351
  return `Usage:
1351
1352
  dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
1352
1353
 
1353
1354
  Ranks nodes related to one or more changed files. <root> is the project root; repeat --changed with project-relative file paths. Results include a combined ranking and the top five related nodes for each changed file. Use --compact for a short text summary or --yml/--yaml for structured agent-facing output.`;
1355
+ case "graph serve":
1356
+ return `Usage:
1357
+ dotdotgod graph serve <root> --changed <path> [--changed <path> ...] [--host HOST] [--port PORT]
1358
+
1359
+ Starts a local-only impact graph explorer. Changed files are rendered as root nodes and are excluded from related-result lists.`;
1354
1360
  case "graph communities":
1355
1361
  return `Usage:
1356
1362
  dotdotgod graph communities <root> [--json]`;
@@ -1374,6 +1380,7 @@ Commands:
1374
1380
  status Show local index status.
1375
1381
  traceability links Check or write generated traceability links.
1376
1382
  graph impact Find files related to changed files.
1383
+ graph serve Open a local impact graph explorer.
1377
1384
  graph communities Find groups of related project-memory nodes.
1378
1385
 
1379
1386
  Options:
@@ -1434,7 +1441,7 @@ function parseCommon(argv) {
1434
1441
 
1435
1442
  // packages/cli/src/validate/run.mjs
1436
1443
  import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2 } from "node:fs";
1437
- import { basename as basename7, dirname as dirname5, extname as extname4, join as join8, resolve as resolve7 } from "node:path";
1444
+ import { basename as basename7, dirname as dirname6, extname as extname4, join as join8, resolve as resolve8 } from "node:path";
1438
1445
 
1439
1446
  // packages/cli/src/docs/traceability.mjs
1440
1447
  import { existsSync as existsSync5 } from "node:fs";
@@ -1533,7 +1540,8 @@ function stripTraceabilityLinksRegion(content) {
1533
1540
  if (region.status === "present") ranges.push({ start: region.start, end: region.end });
1534
1541
  for (const block of extractDotdotgodTraceabilityBlocks(content)) ranges.push({ start: block.index, end: block.end });
1535
1542
  if (ranges.length === 0) return content;
1536
- return ranges.sort((a, b) => b.start - a.start).reduce((next, range) => `${next.slice(0, range.start)}${next.slice(range.end)}`, content);
1543
+ const stripped = ranges.sort((a, b) => b.start - a.start).reduce((next, range) => `${next.slice(0, range.start)}${next.slice(range.end)}`, content);
1544
+ return stripped.replace(/(?:^[ \t]*\r?\n)*^##[ \t]+Traceability[ \t]*\r?(?:\n(?:[ \t]*\r?\n)*|$)/gm, "");
1537
1545
  }
1538
1546
  function markdownLinkPath(root, file, target) {
1539
1547
  const fromDir = dirname2(file);
@@ -1710,7 +1718,7 @@ import { spawnSync } from "node:child_process";
1710
1718
  import { basename as basename3, extname as extname2, join as join5 } from "node:path";
1711
1719
 
1712
1720
  // packages/cli/src/index/constants.mjs
1713
- var CACHE_VERSION = 13;
1721
+ var CACHE_VERSION = 14;
1714
1722
  var CACHE_DIR = ".dotdotgod";
1715
1723
  var MANIFEST_FILE = "manifest.json";
1716
1724
 
@@ -2224,7 +2232,7 @@ function mergeIncrementalGraph(previousGraph, changedGraph, changedPaths) {
2224
2232
  if (!previousGraph) return changedGraph;
2225
2233
  const changedSet = new Set(changedPaths);
2226
2234
  const changedNodeIds = new Set(previousGraph.nodes.filter((node2) => nodeOwnedByPath(node2, changedSet)).map((node2) => node2.id));
2227
- for (const node2 of changedGraph.nodes) changedNodeIds.add(node2.id);
2235
+ for (const node2 of changedGraph.nodes) if (nodeOwnedByPath(node2, changedSet)) changedNodeIds.add(node2.id);
2228
2236
  const graph = { nodes: [], edges: [] };
2229
2237
  for (const node2 of previousGraph.nodes) if (!changedNodeIds.has(node2.id) && !nodeOwnedByPath(node2, changedSet)) addNode(graph, node2.id, node2.type, Object.fromEntries(Object.entries(node2).filter(([key]) => key !== "id" && key !== "type")));
2230
2238
  for (const edge of previousGraph.edges) if (!changedNodeIds.has(edge.source) && !changedNodeIds.has(edge.target)) addEdge(graph, edge.source, edge.target, edge.relation, Object.fromEntries(Object.entries(edge).filter(([key]) => key !== "source" && key !== "target" && key !== "relation")));
@@ -2321,6 +2329,138 @@ function graphSummary(index) {
2321
2329
  return { nodes: graph.nodes.length, edges: graph.edges.length, byType, byRelation };
2322
2330
  }
2323
2331
 
2332
+ // packages/cli/src/validate/references.mjs
2333
+ import { statSync as statSync5 } from "node:fs";
2334
+ import { dirname as dirname5, relative as relative3, resolve as resolve7 } from "node:path";
2335
+ var blank = (text) => text.replace(/[^\n\r]/g, " ");
2336
+ var unescape = (text) => text.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/g, "$1");
2337
+ var labelKey = (text) => unescape(text).trim().replace(/\s+/g, " ").toLowerCase();
2338
+ var escaped = (text, index) => {
2339
+ let count = 0;
2340
+ while (index > 0 && text[--index] === "\\") count++;
2341
+ return count % 2 === 1;
2342
+ };
2343
+ function visibleContent(content) {
2344
+ const region = findTraceabilityLinksRegion(content);
2345
+ let text = region.status === "present" ? content.slice(0, region.start) + blank(content.slice(region.start, region.end)) + content.slice(region.end) : content;
2346
+ const tokens = /<!--[\s\S]*?(?:-->|(?![\s\S]))|^ {0,3}(`{3,}|~{3,})[^\n]*/gm;
2347
+ let token;
2348
+ while (token = tokens.exec(text)) {
2349
+ let end = tokens.lastIndex;
2350
+ if (token[1]) {
2351
+ const closing = new RegExp(`^ {0,3}${token[1][0]}{${token[1].length},}[ \\t]*\\r?$`, "gm");
2352
+ closing.lastIndex = end + 1;
2353
+ end = closing.exec(text) ? closing.lastIndex : text.length;
2354
+ }
2355
+ text = text.slice(0, token.index) + blank(text.slice(token.index, end)) + text.slice(end);
2356
+ tokens.lastIndex = end;
2357
+ }
2358
+ return text;
2359
+ }
2360
+ function destination(text, start) {
2361
+ let i = start;
2362
+ while (/\s/.test(text[i] ?? "") && i < text.length) i++;
2363
+ const begin = i;
2364
+ if (text[i] === "<") {
2365
+ for (++i; i < text.length; i++) {
2366
+ if (text[i] === ">" && !escaped(text, i)) return { href: text.slice(begin + 1, i), end: i + 1 };
2367
+ if (text[i] === "\n") return null;
2368
+ }
2369
+ return null;
2370
+ }
2371
+ let depth = 0;
2372
+ for (; i < text.length; i++) {
2373
+ if (escaped(text, i)) continue;
2374
+ if (/\s/.test(text[i]) || text[i] === ")" && depth === 0) break;
2375
+ if (text[i] === "(") depth++;
2376
+ if (text[i] === ")") depth--;
2377
+ }
2378
+ return depth === 0 && i > begin ? { href: text.slice(begin, i), end: i } : null;
2379
+ }
2380
+ function extractMemoryReferences(content) {
2381
+ let text = visibleContent(content);
2382
+ const references = [];
2383
+ const code = [];
2384
+ const lineAt = (offset) => content.slice(0, offset).split("\n").length;
2385
+ text = text.replace(/(`+)([\s\S]*?)\1(?!`)/g, (match2, ticks, value, offset) => {
2386
+ if (escaped(text, offset)) return match2;
2387
+ code.push({ href: value.trim(), offset, end: offset + match2.length, kind: "inline-code", line: lineAt(offset) });
2388
+ return blank(match2);
2389
+ });
2390
+ const definitions2 = /* @__PURE__ */ new Map();
2391
+ text = text.replace(/^ {0,3}\[([^\]\n]+)\]:[ \t]*(.*)$/gm, (match2, label, value) => {
2392
+ const parsed = destination(value, 0);
2393
+ if (parsed && !definitions2.has(labelKey(label))) definitions2.set(labelKey(label), parsed.href);
2394
+ return blank(match2);
2395
+ });
2396
+ const linkRanges = [];
2397
+ const labels = /!?\[((?:\\.|[^\]\\\n])*)\]/g;
2398
+ let match;
2399
+ while (match = labels.exec(text)) {
2400
+ if (escaped(text, match.index) || match[0][0] === "!" && escaped(text, match.index + 1)) continue;
2401
+ const after = labels.lastIndex;
2402
+ let end = after;
2403
+ let href;
2404
+ if (text[after] === "(") {
2405
+ const parsed = destination(text, after + 1);
2406
+ if (parsed) {
2407
+ const tail = text.slice(parsed.end).match(/^\s*(?:(?:"[^"\n]*"|'[^'\n]*'|\([^\n)]*\))\s*)?\)/);
2408
+ if (tail) {
2409
+ href = parsed.href;
2410
+ end = parsed.end + tail[0].length;
2411
+ }
2412
+ }
2413
+ } else {
2414
+ const suffix = text.slice(after).match(/^\[([^\]\n]*)\]/);
2415
+ const label = suffix?.[1] || match[1];
2416
+ href = definitions2.get(labelKey(label));
2417
+ if (suffix) end += suffix[0].length;
2418
+ }
2419
+ if (href !== void 0) {
2420
+ references.push({ href: unescape(href), kind: "markdown-link", line: lineAt(match.index) });
2421
+ linkRanges.push([match.index, end]);
2422
+ labels.lastIndex = end;
2423
+ }
2424
+ }
2425
+ for (const item of code) {
2426
+ if (linkRanges.some(([start, end]) => item.offset >= start && item.end <= end)) continue;
2427
+ const path = item.href.split("#")[0];
2428
+ if (!path.includes("/") && !/\.[\w-]+$/.test(path) || /[\s*?<>\[\]{}]/.test(path) || path.endsWith("/")) continue;
2429
+ references.push({ href: item.href, kind: item.kind, line: item.line });
2430
+ }
2431
+ return references;
2432
+ }
2433
+ function sharedLocalReferences(content, root, file, config) {
2434
+ const source = resolveMemoryArea(relative3(root, file).split("\\").join("/"), config);
2435
+ if (source?.scope !== "shared") return [];
2436
+ const errors = [];
2437
+ for (const item of extractMemoryReferences(content)) {
2438
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(item.href)) continue;
2439
+ let path;
2440
+ try {
2441
+ path = decodeURIComponent(item.href.split(/[?#]/)[0]);
2442
+ } catch {
2443
+ continue;
2444
+ }
2445
+ if (!path) continue;
2446
+ const base = item.kind === "markdown-link" || /^\.\.?\//.test(path) ? dirname5(file) : root;
2447
+ const target = relative3(root, resolve7(base, path)).split("\\").join("/");
2448
+ if (target === ".." || target.startsWith("../")) continue;
2449
+ if (item.kind === "inline-code") {
2450
+ const configuredDirectory = (config.areas ?? []).some((area3) => (area3.paths ?? []).some((pattern) => pattern === `${target}/**`));
2451
+ let directory = configuredDirectory;
2452
+ try {
2453
+ directory ||= statSync5(resolve7(root, target)).isDirectory();
2454
+ } catch {
2455
+ }
2456
+ if (directory) continue;
2457
+ }
2458
+ const area2 = resolveMemoryArea(target, config);
2459
+ if (area2?.scope === "local") errors.push({ ...item, target, sourceArea: source.id, targetArea: area2.id });
2460
+ }
2461
+ return errors;
2462
+ }
2463
+
2324
2464
  // packages/cli/src/validate/run.mjs
2325
2465
  function runValidate(argv) {
2326
2466
  const options = { root: ".", includeLocalMemory: false, checkIndex: false, maxLines: null, maxChars: null, linkCheck: true, json: false };
@@ -2335,7 +2475,7 @@ function runValidate(argv) {
2335
2475
  else if (!arg.startsWith("-")) options.root = arg;
2336
2476
  else usage(`Unknown option: ${arg}`, "validate");
2337
2477
  }
2338
- const root = resolve7(options.root);
2478
+ const root = resolve8(options.root);
2339
2479
  const memoryConfig = readMemoryConfig(root);
2340
2480
  const documentationRoot = memoryConfig.documentation?.root ?? "docs";
2341
2481
  const docs = join8(root, documentationRoot);
@@ -2399,7 +2539,7 @@ README.md directory indexes, clear HTTP-status filenames such as RESPONSE_200_OK
2399
2539
  walk(docs);
2400
2540
  const dirSiblingMap = /* @__PURE__ */ new Map();
2401
2541
  for (const f of markdownFiles) {
2402
- const dir = dirname5(f);
2542
+ const dir = dirname6(f);
2403
2543
  if (!dirSiblingMap.has(dir)) dirSiblingMap.set(dir, []);
2404
2544
  dirSiblingMap.get(dir).push(basename7(f));
2405
2545
  }
@@ -2415,7 +2555,7 @@ README.md directory indexes, clear HTTP-status filenames such as RESPONSE_200_OK
2415
2555
  const fp = validationPolicy.markdown?.filename ?? {};
2416
2556
  const allow = fp.allow ?? [];
2417
2557
  if (!allow.some((p) => matchPathPattern(rootRel, p)) && (fp.warnNumberedSeries ?? true)) {
2418
- const siblings = dirSiblingMap.get(dirname5(file)) ?? [];
2558
+ const siblings = dirSiblingMap.get(dirname6(file)) ?? [];
2419
2559
  if (isNumberedSeriesFilename(name, siblings)) {
2420
2560
  const heading = extractFirstHeading(content);
2421
2561
  const suggestion = suggestFilenameFromHeading(heading);
@@ -2457,18 +2597,23 @@ ${traceabilityExample()}`, block.line);
2457
2597
  }
2458
2598
  }
2459
2599
  const byDir = /* @__PURE__ */ new Map();
2460
- for (const file of markdownFiles) byDir.set(dirname5(file), [...byDir.get(dirname5(file)) ?? [], file]);
2600
+ for (const file of markdownFiles) byDir.set(dirname6(file), [...byDir.get(dirname6(file)) ?? [], file]);
2461
2601
  for (const [dir, files] of byDir) {
2462
2602
  if (files.length > 1 && !files.some((file) => basename7(file) === "README.md")) addError(dir, "MISSING_README", "Directory with multiple markdown files must include README.md", null, "add a README.md in this directory that indexes the important markdown files and their purpose.");
2463
2603
  }
2604
+ for (const [file, content] of fileCache) {
2605
+ for (const item of sharedLocalReferences(content, root, file, memoryConfig)) {
2606
+ addError(file, "SHARED_LOCAL_MEMORY_REFERENCE", `${item.kind} from shared area "${item.sourceArea}" targets local area "${item.targetArea}": ${item.target}`, item.line, "replace the local reference with shared durable documentation, or remove the dependency. Inline-code directory, glob, and placeholder usage examples are allowed.");
2607
+ }
2608
+ }
2464
2609
  if (options.linkCheck) {
2465
2610
  for (const [file, content] of fileCache) {
2466
- const fileDir = dirname5(file);
2611
+ const fileDir = dirname6(file);
2467
2612
  for (const { href, line } of extractLinks(content)) {
2468
2613
  const hashIndex = href.indexOf("#");
2469
2614
  const pathPart = hashIndex === -1 ? href : href.slice(0, hashIndex);
2470
2615
  const anchor = hashIndex === -1 ? "" : href.slice(hashIndex + 1);
2471
- const target = pathPart ? resolve7(fileDir, pathPart) : file;
2616
+ const target = pathPart ? resolve8(fileDir, pathPart) : file;
2472
2617
  if (pathPart && !existsSync10(target)) {
2473
2618
  addError(file, "BROKEN_LINK", `Local link target does not exist: ${pathPart}`, line, "update the link target to an existing local file, create the intended file, or remove the stale link.");
2474
2619
  continue;
@@ -2538,8 +2683,8 @@ Prompt: ${error.prompt}` : ""}`);
2538
2683
  }
2539
2684
 
2540
2685
  // packages/cli/src/commands/config.mjs
2541
- import { existsSync as existsSync11, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
2542
- import { join as join9, resolve as resolve8 } from "node:path";
2686
+ import { existsSync as existsSync11, statSync as statSync6, writeFileSync as writeFileSync3 } from "node:fs";
2687
+ import { join as join9, resolve as resolve9 } from "node:path";
2543
2688
  function parseConfigOptions(argv, usageKey = "config") {
2544
2689
  const options = { root: ".", json: false, template: null };
2545
2690
  let rootSet = false;
@@ -2557,7 +2702,7 @@ function parseConfigOptions(argv, usageKey = "config") {
2557
2702
  } else if (!arg.startsWith("-")) usage(`Unexpected argument: ${arg}`, usageKey);
2558
2703
  else usage(`Unknown option: ${arg}`, usageKey);
2559
2704
  }
2560
- options.root = resolve8(options.root);
2705
+ options.root = resolve9(options.root);
2561
2706
  return options;
2562
2707
  }
2563
2708
  function configInitError(options, code, message, path = null) {
@@ -2602,7 +2747,7 @@ function runConfig(argv) {
2602
2747
  if (isInit) {
2603
2748
  if (!existsSync11(options.root)) configInitError(options, "ROOT_NOT_FOUND", `Project root not found: ${options.root}`);
2604
2749
  try {
2605
- if (!statSync5(options.root).isDirectory()) configInitError(options, "ROOT_NOT_DIRECTORY", `Project root is not a directory: ${options.root}`);
2750
+ if (!statSync6(options.root).isDirectory()) configInitError(options, "ROOT_NOT_DIRECTORY", `Project root is not a directory: ${options.root}`);
2606
2751
  } catch {
2607
2752
  configInitError(options, "ROOT_NOT_FOUND", `Project root not found: ${options.root}`);
2608
2753
  }
@@ -2633,11 +2778,11 @@ function runConfig(argv) {
2633
2778
  }
2634
2779
 
2635
2780
  // packages/cli/src/commands/query.mjs
2636
- import { resolve as resolve9 } from "node:path";
2781
+ import { resolve as resolve10 } from "node:path";
2637
2782
 
2638
2783
  // packages/cli/src/query/chunks.mjs
2639
2784
  import { readFileSync as readFileSync10, readdirSync as readdirSync3 } from "node:fs";
2640
- import { join as join10, relative as relative3 } from "node:path";
2785
+ import { join as join10, relative as relative4 } from "node:path";
2641
2786
  import { createHash as createHash3 } from "node:crypto";
2642
2787
  var MAX_CHUNK_CHARS = 1600;
2643
2788
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", ".git", ".dotdotgod"]);
@@ -2658,7 +2803,7 @@ function collectDocumentationMarkdown(root, exclude = ["docs/plan", "docs/archiv
2658
2803
  for (const entry of entries) {
2659
2804
  if (entry.name.startsWith(".")) continue;
2660
2805
  const absolute = join10(directory, entry.name);
2661
- const path = relative3(root, absolute).replaceAll("\\", "/");
2806
+ const path = relative4(root, absolute).replaceAll("\\", "/");
2662
2807
  if (isSecretLikePathPattern(path) || exclude.some((pattern) => matchPathPattern(path, pattern) || path === pattern || path.startsWith(`${pattern}/`))) continue;
2663
2808
  if (entry.isDirectory()) {
2664
2809
  if (!SKIPPED_DIRECTORIES.has(entry.name)) walk(absolute);
@@ -2951,7 +3096,7 @@ function parseQueryOptions(argv) {
2951
3096
  options.limit = value;
2952
3097
  } else if (arg.startsWith("-")) usage(`Unknown option: ${arg}`, "query");
2953
3098
  else if (!rootSet) {
2954
- options.root = resolve9(arg);
3099
+ options.root = resolve10(arg);
2955
3100
  rootSet = true;
2956
3101
  } else queryParts.push(arg);
2957
3102
  }
@@ -3062,19 +3207,19 @@ async function runQuery(argv) {
3062
3207
  }
3063
3208
 
3064
3209
  // packages/cli/src/commands/embedding.mjs
3065
- import { resolve as resolve10 } from "node:path";
3210
+ import { resolve as resolve11 } from "node:path";
3066
3211
  function parse(argv) {
3067
3212
  const [action2 = "status", ...rest] = argv;
3068
3213
  let root = ".";
3069
- let json = false;
3214
+ let json2 = false;
3070
3215
  let confirm = false;
3071
3216
  for (const arg of rest) {
3072
- if (arg === "--json") json = true;
3217
+ if (arg === "--json") json2 = true;
3073
3218
  else if (arg === "--confirm") confirm = true;
3074
3219
  else if (!arg.startsWith("-") && root === ".") root = arg;
3075
3220
  else usage(`Unknown embedding option: ${arg}`, `embedding ${action2}`);
3076
3221
  }
3077
- return { action: action2, root: resolve10(root), json, confirm };
3222
+ return { action: action2, root: resolve11(root), json: json2, confirm };
3078
3223
  }
3079
3224
  function runEmbedding(argv, options = {}) {
3080
3225
  const parsed = parse(argv);
@@ -3094,11 +3239,11 @@ function runEmbedding(argv, options = {}) {
3094
3239
  }
3095
3240
 
3096
3241
  // packages/cli/src/commands/map.mjs
3097
- import { resolve as resolve12 } from "node:path";
3242
+ import { resolve as resolve13 } from "node:path";
3098
3243
 
3099
3244
  // packages/cli/src/memory/documentation-map.mjs
3100
- import { existsSync as existsSync14, readdirSync as readdirSync4, statSync as statSync6 } from "node:fs";
3101
- import { join as join13, resolve as resolve11 } from "node:path";
3245
+ import { existsSync as existsSync14, readdirSync as readdirSync4, statSync as statSync7 } from "node:fs";
3246
+ import { join as join13, resolve as resolve12 } from "node:path";
3102
3247
  var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", ".git", ".dotdotgod"]);
3103
3248
  function node() {
3104
3249
  return { directories: /* @__PURE__ */ new Map(), files: [] };
@@ -3178,10 +3323,10 @@ function discoverMarkdown(root, documentationRoot, exclude) {
3178
3323
  return paths.sort();
3179
3324
  }
3180
3325
  function buildDocumentationMap(projectRoot = ".", { depth = 5 } = {}) {
3181
- const root = resolve11(projectRoot);
3326
+ const root = resolve12(projectRoot);
3182
3327
  if (!existsSync14(root)) return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3183
3328
  try {
3184
- if (!statSync6(root).isDirectory()) return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3329
+ if (!statSync7(root).isDirectory()) return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3185
3330
  } catch {
3186
3331
  return { ok: false, error: { code: "ROOT_NOT_FOUND", message: `Project root not found: ${root}` } };
3187
3332
  }
@@ -3201,8 +3346,8 @@ function buildDocumentationMap(projectRoot = ".", { depth = 5 } = {}) {
3201
3346
  }
3202
3347
 
3203
3348
  // packages/cli/src/commands/map.mjs
3204
- function fail(json, code, message) {
3205
- if (json) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
3349
+ function fail(json2, code, message) {
3350
+ if (json2) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
3206
3351
  else console.error(message);
3207
3352
  process.exit(2);
3208
3353
  }
@@ -3227,7 +3372,7 @@ function parseMapOptions(argv) {
3227
3372
  } else if (!arg.startsWith("-")) fail(options.json, "UNEXPECTED_ARGUMENT", `Unexpected argument: ${arg}`);
3228
3373
  else fail(options.json, "UNKNOWN_OPTION", `Unknown option: ${arg}`);
3229
3374
  }
3230
- options.root = resolve12(options.root);
3375
+ options.root = resolve13(options.root);
3231
3376
  return options;
3232
3377
  }
3233
3378
  function runMap(argv) {
@@ -3239,7 +3384,7 @@ function runMap(argv) {
3239
3384
  }
3240
3385
 
3241
3386
  // packages/cli/src/reference/resolve.mjs
3242
- import { resolve as resolve14 } from "node:path";
3387
+ import { resolve as resolve15 } from "node:path";
3243
3388
 
3244
3389
  // node_modules/.pnpm/leiden-ts@0.1.0/node_modules/leiden-ts/dist/index.js
3245
3390
  var GraphValidationError = class extends Error {
@@ -4475,8 +4620,8 @@ function buildCommunities(index, limits = {}) {
4475
4620
  byCommunity.get(communityId).push(projection.durable[index2]);
4476
4621
  });
4477
4622
  const communities = [...byCommunity.entries()].map(([communityId, nodes]) => {
4478
- const labelKey = labelForLeidenCommunity(nodes, config);
4479
- const community = makeCommunity(`leiden-${communityId}`, communityLabel(labelKey));
4623
+ const labelKey2 = labelForLeidenCommunity(nodes, config);
4624
+ const community = makeCommunity(`leiden-${communityId}`, communityLabel(labelKey2));
4480
4625
  for (const node2 of nodes.sort((a, b) => a.id.localeCompare(b.id))) addCommunityDetails(community, node2, itemLimit, config);
4481
4626
  return community;
4482
4627
  });
@@ -4611,14 +4756,14 @@ function compareImpactItems(seeds) {
4611
4756
 
4612
4757
  // packages/cli/src/impact/vector-profile.mjs
4613
4758
  import { lstatSync, readFileSync as readFileSync13, realpathSync } from "node:fs";
4614
- import { extname as extname5, relative as relative4, resolve as resolve13, sep } from "node:path";
4759
+ import { extname as extname5, relative as relative5, resolve as resolve14, sep } from "node:path";
4615
4760
  var MAX_VECTOR_PROFILE_FILE_BYTES = 64 * 1024;
4616
4761
  var MAX_VECTOR_PROFILE_CHARS = 4e3;
4617
4762
  var MAX_VECTOR_PROFILE_METADATA_ITEMS = 20;
4618
4763
  var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".c", ".cc", ".cpp", ".css", ".go", ".h", ".hpp", ".html", ".java", ".js", ".json", ".jsx", ".md", ".mjs", ".py", ".rb", ".rs", ".sh", ".ts", ".tsx", ".txt", ".yaml", ".yml"]);
4619
4764
  var GENERATED_SEGMENTS = /* @__PURE__ */ new Set([".dotdotgod", "build", "coverage", "dist", "node_modules"]);
4620
4765
  function insideRoot(root, absolute) {
4621
- const normalizedRoot = resolve13(root);
4766
+ const normalizedRoot = resolve14(root);
4622
4767
  return absolute === normalizedRoot || absolute.startsWith(`${normalizedRoot}${sep}`);
4623
4768
  }
4624
4769
  function normalizeChangedPath(path) {
@@ -4629,17 +4774,17 @@ function normalizeChangedPath(path) {
4629
4774
  function canonicalizeChangedPath(root, path) {
4630
4775
  const normalized = normalizeChangedPath(path);
4631
4776
  if (!normalized) return null;
4632
- const rootAbsolute = resolve13(root);
4633
- const absolute = resolve13(rootAbsolute, normalized);
4777
+ const rootAbsolute = resolve14(root);
4778
+ const absolute = resolve14(rootAbsolute, normalized);
4634
4779
  if (!insideRoot(rootAbsolute, absolute)) return normalized;
4635
4780
  try {
4636
4781
  const canonicalRoot = realpathSync(rootAbsolute);
4637
4782
  const canonicalAbsolute = realpathSync(absolute);
4638
4783
  if (!insideRoot(canonicalRoot, canonicalAbsolute)) return normalized;
4639
- const canonical = relative4(canonicalRoot, canonicalAbsolute).replaceAll("\\", "/");
4784
+ const canonical = relative5(canonicalRoot, canonicalAbsolute).replaceAll("\\", "/");
4640
4785
  return canonical || normalized;
4641
4786
  } catch {
4642
- return relative4(rootAbsolute, absolute).replaceAll("\\", "/") || normalized;
4787
+ return relative5(rootAbsolute, absolute).replaceAll("\\", "/") || normalized;
4643
4788
  }
4644
4789
  }
4645
4790
  function unsafeProfilePath(path) {
@@ -4661,11 +4806,11 @@ function graphMetadata(graph, seedId) {
4661
4806
  function safeTextPrefix(root, path) {
4662
4807
  if (unsafeProfilePath(path) || !TEXT_EXTENSIONS.has(extname5(path).toLowerCase())) return "";
4663
4808
  try {
4664
- const absolute = resolve13(root, path);
4809
+ const absolute = resolve14(root, path);
4665
4810
  if (!insideRoot(root, absolute)) return "";
4666
4811
  const canonicalAbsolute = realpathSync(absolute);
4667
4812
  if (!insideRoot(realpathSync(root), canonicalAbsolute)) return "";
4668
- const canonicalPath = relative4(realpathSync(root), canonicalAbsolute).replaceAll("\\", "/");
4813
+ const canonicalPath = relative5(realpathSync(root), canonicalAbsolute).replaceAll("\\", "/");
4669
4814
  if (canonicalPath !== path || unsafeProfilePath(canonicalPath) || !TEXT_EXTENSIONS.has(extname5(canonicalPath).toLowerCase())) return "";
4670
4815
  const stats = lstatSync(canonicalAbsolute);
4671
4816
  if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_VECTOR_PROFILE_FILE_BYTES) return "";
@@ -4788,7 +4933,7 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4788
4933
  const nodeById = new Map(graph.nodes.map((node2) => [node2.id, node2]));
4789
4934
  const seeds = changedPaths.map((path) => `file:${path}`);
4790
4935
  const seedSet = new Set(seeds);
4791
- const maxRelated = Math.max(limits.related ?? 25, seeds.length);
4936
+ const maxRelated = Math.max(limits.related ?? 25, 0);
4792
4937
  const groups = { files: { items: [], omitted: 0 }, docs: { items: [], omitted: 0 }, contracts: { items: [], omitted: 0 }, tests: { items: [], omitted: 0 }, commands: { items: [], omitted: 0 }, events: { items: [], omitted: 0 }, packageResources: { items: [], omitted: 0 }, symbols: { items: [], omitted: 0 } };
4793
4938
  const relatedIds = new Set(seeds);
4794
4939
  const reasons = new Map(seeds.map((seed) => [seed, /* @__PURE__ */ new Set(["changed-file"])]));
@@ -4824,7 +4969,8 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4824
4969
  const scored = scoreImpactItem({ ...node2, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores, config);
4825
4970
  return { ...node2, reasons: reasonList, hasCuratedEvidence, ...vectorEvidence.has(id) ? { vectorEvidence: vectorEvidence.get(id) } : {}, retrieval: { ...retrieval, signals: [.../* @__PURE__ */ new Set([...retrieval.signals ?? [], ...reasonSignals])] }, ...scored };
4826
4971
  }).sort(compareImpactItems(seeds));
4827
- const related = selectImpactItems(relatedAll, maxRelated, seeds);
4972
+ const rankedRelated = relatedAll.filter((item) => !seedSet.has(item.id));
4973
+ const related = selectImpactItems(rankedRelated, maxRelated, []);
4828
4974
  for (const item of related) {
4829
4975
  if (item.type === "file") {
4830
4976
  const area2 = docsArea2(item.path, config);
@@ -4846,9 +4992,9 @@ function buildImpactReport(index, changedPaths, limits = {}) {
4846
4992
  aggregate2.perSeed = normalized.map((changed) => {
4847
4993
  const seedId = `file:${changed}`;
4848
4994
  const seedOverlay = limits.overlay ? { ...limits.overlay, edges: (limits.overlay.edges ?? []).filter((edge) => edge.source === seedId) } : void 0;
4849
- const report = buildCombinedImpactReport(index, [changed], { ...limits, overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit + 1) });
4850
- const related = report.related.filter((item) => item.id !== `file:${changed}`).slice(0, perSeedLimit);
4851
- return { changed, related, omittedRelated: Math.max(0, report.related.length - 1 - related.length) + report.omittedRelated };
4995
+ const report = buildCombinedImpactReport(index, [changed], { ...limits, overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit) });
4996
+ const related = report.related.slice(0, perSeedLimit);
4997
+ return { changed, related, omittedRelated: Math.max(0, report.related.length - related.length) + report.omittedRelated };
4852
4998
  });
4853
4999
  return aggregate2;
4854
5000
  }
@@ -4868,7 +5014,7 @@ function compactImpactGroup(group = { items: [], omitted: 0 }, limit = 5) {
4868
5014
  return { items, omitted: (group.omitted ?? 0) + Math.max(0, (group.items?.length ?? 0) - items.length) };
4869
5015
  }
4870
5016
  function buildCompactImpactReport(impact, limits = {}) {
4871
- const relatedLimit = Math.max(limits.related ?? 10, impact.changedFiles?.length ?? 1);
5017
+ const relatedLimit = limits.related ?? 10;
4872
5018
  const groupLimit = limits.groupItems ?? 5;
4873
5019
  const changedFiles = impact.changedFiles ?? [impact.changed];
4874
5020
  const seedIds = new Set(changedFiles.map((path) => `file:${path}`));
@@ -4881,7 +5027,7 @@ function buildCompactImpactReport(impact, limits = {}) {
4881
5027
  }
4882
5028
 
4883
5029
  // packages/cli/src/reference/extract.mjs
4884
- import { basename as basename8, dirname as dirname6, extname as extname6 } from "node:path";
5030
+ import { basename as basename8, dirname as dirname7, extname as extname6 } from "node:path";
4885
5031
  var DEFAULT_REFERENCE_LIMIT = 5;
4886
5032
  function extractBracketReferences(prompt = "") {
4887
5033
  const refs = [];
@@ -4932,7 +5078,7 @@ function aliasEntriesForPath(path = "") {
4932
5078
  const parts = withoutMd.split("/").filter(Boolean);
4933
5079
  for (let i = 0; i < parts.length; i += 1) entries.push({ alias: parts.slice(i).join("/"), kind: "path-suffix" });
4934
5080
  if (base === "README.md") {
4935
- const dir = dirname6(path).replace(/\\/g, "/");
5081
+ const dir = dirname7(path).replace(/\\/g, "/");
4936
5082
  entries.push({ alias: basename8(dir), kind: "path" });
4937
5083
  }
4938
5084
  for (const alias of [path, withoutMd, base, stem]) entries.push({ alias, kind: "path" });
@@ -4986,8 +5132,8 @@ function extractFuzzyReferences(prompt = "", index = null, options = {}) {
4986
5132
  }
4987
5133
  }
4988
5134
  for (const { alias } of [...aliasByKey.values()].sort((a, b) => b.priority - a.priority || b.alias.length - a.alias.length)) {
4989
- const escaped = alias.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "[\\s_-]+");
4990
- const pattern = new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, "i");
5135
+ const escaped2 = alias.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "[\\s_-]+");
5136
+ const pattern = new RegExp(`(^|[^a-z0-9])${escaped2}([^a-z0-9]|$)`, "i");
4991
5137
  if (pattern.test(lower)) add(alias, "known_alias", "medium");
4992
5138
  if (refs.length >= (options.maxFuzzyRefs ?? 5)) break;
4993
5139
  }
@@ -5062,7 +5208,7 @@ function parseReferenceOptions(argv, command) {
5062
5208
  else filtered.push(arg);
5063
5209
  }
5064
5210
  const operands = filtered.filter((arg) => !arg.startsWith("-"));
5065
- return { ...options, root: resolve14(operands[0] ?? "."), json: filtered.includes("--json"), rootArgv: operands.slice(1) };
5211
+ return { ...options, root: resolve15(operands[0] ?? "."), json: filtered.includes("--json"), rootArgv: operands.slice(1) };
5066
5212
  }
5067
5213
  function formatReferenceOutput(payload) {
5068
5214
  const refreshNote = payload.metadata.cacheRefreshed ? ", refreshed" : "";
@@ -5112,7 +5258,7 @@ function runExpand(argv) {
5112
5258
 
5113
5259
  // packages/cli/src/commands/traceability.mjs
5114
5260
  import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
5115
- import { join as join14, resolve as resolve15 } from "node:path";
5261
+ import { join as join14, resolve as resolve16 } from "node:path";
5116
5262
  function collectDocsMarkdownFiles(root) {
5117
5263
  const docs = join14(root, "docs");
5118
5264
  const files = [];
@@ -5144,7 +5290,7 @@ function parseTraceabilityOptions(argv) {
5144
5290
  }
5145
5291
  if (options.check && options.write) usage("Choose only one traceability links mode: --check or --write.", "traceability links");
5146
5292
  options.check = options.check || !options.write;
5147
- options.root = resolve15(options.root);
5293
+ options.root = resolve16(options.root);
5148
5294
  return options;
5149
5295
  }
5150
5296
  function runTraceability(argv) {
@@ -5183,6 +5329,187 @@ function runTraceability(argv) {
5183
5329
  process.exit(ok ? 0 : 1);
5184
5330
  }
5185
5331
 
5332
+ // packages/cli/src/graph-view/server.mjs
5333
+ import { createServer } from "node:http";
5334
+ import { readFileSync as readFileSync15 } from "node:fs";
5335
+ import { dirname as dirname8, extname as extname7, join as join15 } from "node:path";
5336
+ import { fileURLToPath } from "node:url";
5337
+
5338
+ // packages/cli/src/graph-view/payload.mjs
5339
+ function edgeKey(edge) {
5340
+ return `${edge.source}\0${edge.target}\0${edge.relation}`;
5341
+ }
5342
+ function labelForNode(node2) {
5343
+ if (node2.type === "contract") return node2.title ?? node2.contractId ?? node2.id;
5344
+ return node2.path ?? node2.command ?? node2.name ?? node2.target ?? node2.id;
5345
+ }
5346
+ function adjacencyFor(nodes, edges) {
5347
+ const adjacency = new Map(nodes.map((node2) => [node2.id, /* @__PURE__ */ new Set()]));
5348
+ for (const edge of edges) {
5349
+ adjacency.get(edge.source)?.add(edge.target);
5350
+ adjacency.get(edge.target)?.add(edge.source);
5351
+ }
5352
+ return adjacency;
5353
+ }
5354
+ function rootReachability(nodes, edges, seedIds) {
5355
+ const adjacency = adjacencyFor(nodes, edges);
5356
+ const reachable = /* @__PURE__ */ new Set();
5357
+ const depthByNode = /* @__PURE__ */ new Map();
5358
+ const nearestRootByNode = /* @__PURE__ */ new Map();
5359
+ const queue = [];
5360
+ for (const node2 of nodes) {
5361
+ if (!seedIds.has(node2.id)) continue;
5362
+ reachable.add(node2.id);
5363
+ depthByNode.set(node2.id, 0);
5364
+ nearestRootByNode.set(node2.id, node2.id);
5365
+ queue.push(node2.id);
5366
+ }
5367
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
5368
+ const current = queue[cursor];
5369
+ for (const neighbor of adjacency.get(current) ?? []) {
5370
+ if (reachable.has(neighbor)) continue;
5371
+ reachable.add(neighbor);
5372
+ depthByNode.set(neighbor, (depthByNode.get(current) ?? 0) + 1);
5373
+ nearestRootByNode.set(neighbor, nearestRootByNode.get(current));
5374
+ queue.push(neighbor);
5375
+ }
5376
+ }
5377
+ return { reachable, depthByNode, nearestRootByNode };
5378
+ }
5379
+ function buildImpactGraphPayload(index, impact) {
5380
+ const graph = index?.graph ?? { nodes: [], edges: [] };
5381
+ const config = index?.memoryConfig;
5382
+ const seedIds = new Set((impact.changedFiles ?? [impact.changed]).map((path) => `file:${path}`));
5383
+ const impactItems = new Map((impact.related ?? []).map((item) => [item.id, item]));
5384
+ const isAllowedNode = (node2) => {
5385
+ if (!node2) return false;
5386
+ if (node2.path && resolveMemoryArea(node2.path, config)?.scope === "local") return false;
5387
+ return true;
5388
+ };
5389
+ const sourceNodes = (graph.nodes ?? []).filter(isAllowedNode);
5390
+ const indexedNodes = new Map(sourceNodes.map((node2) => [node2.id, node2]));
5391
+ const seenEdges = /* @__PURE__ */ new Set();
5392
+ const edges = [];
5393
+ for (const edge of graph.edges ?? []) {
5394
+ if (!indexedNodes.has(edge.source) || !indexedNodes.has(edge.target)) continue;
5395
+ const key = edgeKey(edge);
5396
+ if (seenEdges.has(key)) continue;
5397
+ seenEdges.add(key);
5398
+ edges.push({ source: edge.source, target: edge.target, relation: edge.relation, confidence: edge.confidence, weight: edge.weight ?? edge.relationWeight, score: edge.score, traceabilityKey: edge.traceabilityKey });
5399
+ }
5400
+ const { reachable, depthByNode, nearestRootByNode } = rootReachability(sourceNodes, edges, seedIds);
5401
+ const retainedNodes = sourceNodes.filter((node2) => reachable.has(node2.id));
5402
+ const retainedEdges = edges.filter((edge) => reachable.has(edge.source) && reachable.has(edge.target));
5403
+ const nodes = retainedNodes.map((source) => {
5404
+ const item = impactItems.get(source.id);
5405
+ const seed = seedIds.has(source.id);
5406
+ return {
5407
+ id: source.id,
5408
+ type: source.type ?? item?.type ?? "file",
5409
+ path: source.path ?? item?.path,
5410
+ label: labelForNode({ ...source, ...item }),
5411
+ seed,
5412
+ ranked: Boolean(item),
5413
+ depth: depthByNode.get(source.id) ?? 0,
5414
+ nearestRoot: nearestRootByNode.get(source.id),
5415
+ component: 0,
5416
+ seededComponent: true,
5417
+ impactScore: seed ? 100 : item?.impactScore ?? 0,
5418
+ reasons: seed ? ["changed-file"] : item?.reasons ?? [],
5419
+ scoreBreakdown: seed ? { seed: 100 } : item?.scoreBreakdown,
5420
+ retrieval: item?.retrieval ?? source.retrieval,
5421
+ contractId: item?.contractId ?? source.contractId,
5422
+ title: item?.title ?? source.title,
5423
+ sections: item?.sections ?? source.sections,
5424
+ vectorEvidence: item?.vectorEvidence
5425
+ };
5426
+ });
5427
+ return {
5428
+ changed: impact.changed,
5429
+ changedFiles: impact.changedFiles ?? [impact.changed],
5430
+ ranking: impact.ranking,
5431
+ semantic: impact.semantic,
5432
+ complete: true,
5433
+ nodes,
5434
+ edges: retainedEdges,
5435
+ components: retainedNodes.length > 0 ? [{ id: 0, size: retainedNodes.length, seeded: true }] : [],
5436
+ diagnostics: {
5437
+ rootNodes: retainedNodes.filter((node2) => seedIds.has(node2.id)).length,
5438
+ connectedNodes: retainedNodes.length,
5439
+ disconnectedNodesOmitted: sourceNodes.length - retainedNodes.length,
5440
+ maximumDepth: Math.max(0, ...depthByNode.values())
5441
+ },
5442
+ omittedRelated: impact.omittedRelated ?? 0
5443
+ };
5444
+ }
5445
+
5446
+ // packages/cli/src/graph-view/server.mjs
5447
+ var ASSET_ROOT = join15(dirname8(fileURLToPath(import.meta.url)), "assets");
5448
+ var CONTENT_TYPES = { ".css": "text/css; charset=utf-8", ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".map": "application/json; charset=utf-8" };
5449
+ var VENDOR_FILES = {
5450
+ "/vendor/sigma.js": new URL("../../node_modules/sigma/dist/sigma.min.js", import.meta.url),
5451
+ "/vendor/graphology.js": new URL("../../node_modules/graphology/dist/graphology.umd.min.js", import.meta.url),
5452
+ "/vendor/d3.js": new URL("../../node_modules/d3/dist/d3.min.js", import.meta.url)
5453
+ };
5454
+ function json(response, status, value) {
5455
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
5456
+ response.end(JSON.stringify(value));
5457
+ }
5458
+ function asset(response, file) {
5459
+ try {
5460
+ const body = readFileSync15(file);
5461
+ response.writeHead(200, { "content-type": CONTENT_TYPES[extname7(file)] ?? "application/octet-stream", "cache-control": "no-store" });
5462
+ response.end(body);
5463
+ } catch {
5464
+ response.writeHead(404);
5465
+ response.end("Not found");
5466
+ }
5467
+ }
5468
+ async function buildServedImpact(root, changedPaths) {
5469
+ const normalized = [...new Set(changedPaths.map((path) => canonicalizeChangedPath(root, path)).filter(Boolean))];
5470
+ const { status, index, metadata } = readFreshIndex(root);
5471
+ const overlay = await buildVectorImpactOverlay(root, index, normalized);
5472
+ const impact = buildImpactReport(index, normalized, { overlay, verboseSemantic: false, related: 40 });
5473
+ const graph = buildImpactGraphPayload(index, impact);
5474
+ const graphIds = new Set(graph.nodes.map((node2) => node2.id));
5475
+ const explorerImpact = {
5476
+ ...impact,
5477
+ related: impact.related.filter((item) => graphIds.has(item.id))
5478
+ };
5479
+ return { ok: status.ok, status, metadata, impact: explorerImpact, graph };
5480
+ }
5481
+ async function startImpactGraphServer({ root, changed, host = "127.0.0.1", port = 0 }) {
5482
+ let payload = await buildServedImpact(root, changed);
5483
+ const server = createServer(async (request, response) => {
5484
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? host}`);
5485
+ if (url.pathname === "/api/impact") return json(response, 200, payload);
5486
+ if (url.pathname === "/api/reroot") {
5487
+ const paths = url.searchParams.getAll("changed");
5488
+ if (paths.length === 0) return json(response, 400, { ok: false, error: { code: "MISSING_CHANGED", message: "At least one changed path is required." } });
5489
+ try {
5490
+ payload = await buildServedImpact(root, paths);
5491
+ return json(response, 200, payload);
5492
+ } catch (error) {
5493
+ return json(response, 500, { ok: false, error: { code: "GRAPH_SERVE_FAILED", message: error instanceof Error ? error.message : String(error) } });
5494
+ }
5495
+ }
5496
+ if (VENDOR_FILES[url.pathname]) return asset(response, fileURLToPath(VENDOR_FILES[url.pathname]));
5497
+ const relative6 = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
5498
+ if (!["index.html", "app.css", "app.js"].includes(relative6)) {
5499
+ response.writeHead(404);
5500
+ return response.end("Not found");
5501
+ }
5502
+ return asset(response, join15(ASSET_ROOT, relative6));
5503
+ });
5504
+ await new Promise((resolve17, reject) => {
5505
+ server.once("error", reject);
5506
+ server.listen(port, host, resolve17);
5507
+ });
5508
+ const address = server.address();
5509
+ const actualPort = typeof address === "object" && address ? address.port : port;
5510
+ return { server, url: `http://${host}:${actualPort}`, payload };
5511
+ }
5512
+
5186
5513
  // packages/cli/src/impact/format.mjs
5187
5514
  function formatCompactImpactGroup(name, group) {
5188
5515
  const items = group?.items ?? [];
@@ -5290,6 +5617,8 @@ function parseGraphOptions(argv) {
5290
5617
  const changed = [];
5291
5618
  let compact = false;
5292
5619
  let yml = false;
5620
+ let host = "127.0.0.1";
5621
+ let port = 0;
5293
5622
  for (let i = 0; i < argv.length; i += 1) {
5294
5623
  if (argv[i] === "--changed") {
5295
5624
  const next = argv[i + 1];
@@ -5299,19 +5628,36 @@ function parseGraphOptions(argv) {
5299
5628
  }
5300
5629
  } else if (argv[i] === "--compact") compact = true;
5301
5630
  else if (argv[i] === "--yml" || argv[i] === "--yaml") yml = true;
5302
- else filtered.push(argv[i]);
5631
+ else if (argv[i] === "--host" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
5632
+ host = argv[i + 1];
5633
+ i += 1;
5634
+ } else if (argv[i] === "--port" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
5635
+ port = Number(argv[i + 1]);
5636
+ i += 1;
5637
+ } else filtered.push(argv[i]);
5303
5638
  }
5304
5639
  const options = parseCommon(filtered);
5305
5640
  options.changed = [...new Set(changed)];
5306
5641
  options.compact = compact;
5307
5642
  options.yml = yml;
5643
+ options.host = host;
5644
+ options.port = Number.isInteger(port) && port >= 0 && port <= 65535 ? port : NaN;
5308
5645
  return options;
5309
5646
  }
5310
5647
  async function runGraph(argv) {
5311
5648
  const sub = argv[0];
5312
5649
  const isImpact = sub === "impact";
5313
- if (!["impact", "communities"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
5650
+ const isServe = sub === "serve";
5651
+ if (!["impact", "communities", "serve"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
5314
5652
  const options = parseGraphOptions(argv.slice(1));
5653
+ if (isServe && options.changed.length === 0) usage("Missing required option: --changed <path>.", "graph serve");
5654
+ if (isServe && !Number.isInteger(options.port)) usage("Invalid --port. Use an integer from 0 through 65535.", "graph serve");
5655
+ if (isServe) {
5656
+ options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
5657
+ const running = await startImpactGraphServer({ root: options.root, changed: options.changed, host: options.host, port: options.port });
5658
+ console.log(`graph serve: ${running.url}`);
5659
+ return;
5660
+ }
5315
5661
  if (isImpact && [options.json, options.compact, options.yml].filter(Boolean).length > 1) {
5316
5662
  const message = "Choose only one graph impact output mode: --compact, --json, or --yml/--yaml.";
5317
5663
  if (options.json) console.log(JSON.stringify({ ok: false, command: "graph impact", compact: options.compact || void 0, yml: options.yml || void 0, root: options.root, error: { code: "OUTPUT_MODE_CONFLICT", message }, usage: commandUsage("graph impact") }, null, 2));
package/mcp/server.mjs CHANGED
@@ -23157,7 +23157,7 @@ function markdownTree(root2, maxDepth = 5) {
23157
23157
  walk(docs, 1);
23158
23158
  return output;
23159
23159
  }
23160
- async function cliJson(args, cwd) {
23160
+ async function cliText(args, cwd) {
23161
23161
  return await new Promise((resolvePromise, reject) => {
23162
23162
  const child = spawn2(process.execPath, [cli, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"] });
23163
23163
  const stdout = [];
@@ -23168,14 +23168,18 @@ async function cliJson(args, cwd) {
23168
23168
  child.once("close", (code) => {
23169
23169
  const out = Buffer.concat(stdout).toString("utf8");
23170
23170
  if (code !== 0) return reject(new Error(Buffer.concat(stderr).toString("utf8") || out || `dotdotgod exited ${code}`));
23171
- try {
23172
- resolvePromise(JSON.parse(out));
23173
- } catch {
23174
- reject(new Error(`Expected JSON from dotdotgod: ${out.slice(0, 500)}`));
23175
- }
23171
+ resolvePromise(out);
23176
23172
  });
23177
23173
  });
23178
23174
  }
23175
+ async function cliJson(args, cwd) {
23176
+ const out = await cliText(args, cwd);
23177
+ try {
23178
+ return JSON.parse(out);
23179
+ } catch {
23180
+ throw new Error(`Expected JSON from dotdotgod: ${out.slice(0, 500)}`);
23181
+ }
23182
+ }
23179
23183
  async function projectLoad(input) {
23180
23184
  const root2 = resolve5(input.root || process.cwd());
23181
23185
  const focus = input.focus?.trim() ?? "";
@@ -23217,8 +23221,8 @@ async function projectImpact(input) {
23217
23221
  if (paths.length > 20) throw new Error("paths is limited to 20 entries per call");
23218
23222
  const args = ["graph", "impact", root2];
23219
23223
  for (const path of paths) args.push("--changed", path);
23220
- args.push("--json");
23221
- return await cliJson(args, root2);
23224
+ args.push("--compact");
23225
+ return { ok: true, summary: (await cliText(args, root2)).trim() };
23222
23226
  }
23223
23227
  async function projectInitialize(input) {
23224
23228
  const root2 = resolve5(input.root || process.cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/claude-code",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Claude Code adapter for dotdotgod project memory workflows.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "esbuild": "0.27.3",
28
- "@dotdotgod/context": "0.3.9"
28
+ "@dotdotgod/context": "0.3.11"
29
29
  },
30
30
  "files": [
31
31
  ".claude-plugin",
@@ -10,20 +10,31 @@ version: 1.0.0
10
10
 
11
11
  ## Memory-Area Context
12
12
 
13
- Run `dotdotgod config <root> --json` and use the resolved `config.areas`. Match the target path against the ordered areas and use the first match. If the CLI is unavailable, continue from the target document, nearest README, and direct links.
13
+ Use `dotdotgod config <root> --json` (or the source-checkout CLI equivalent) to obtain resolved `config.areas`; reuse current resolved settings already in context when the project/config has not changed. Normalize the target to a repository-relative path. Check areas in order, applying each area's `excludePaths` before selecting the first matching `paths` entry. Keep the matched area's relevant guidance in context rather than repeatedly injecting the entire configuration. If no area matches, use the document's purpose and repository conventions without inventing metadata.
14
14
 
15
15
  ## Goal
16
16
 
17
17
  Make project documentation easier to understand and act on while preserving established behavior, decisions, task state, historical meaning, and traceability.
18
18
 
19
+ ## Editing Checklist
20
+
21
+ Adapted from [Google Technical Writing One](https://developers.google.com/tech-writing/one); these principles supplement repository policy. Apply them directly without requiring runtime web access.
22
+
23
+ - **Audience:** distinguish what readers already know from what they need to learn or do. Add only evidence-backed context that closes that gap.
24
+ - **Terminology:** use one consistent name per concept; explain unfamiliar terms and abbreviations when readers need them. Preserve exact API names and identifiers.
25
+ - **Sentences:** use specific verbs and make responsibility clear. Prefer active voice when it helps; retain passive voice when appropriate and never invent an unknown actor. Keep one main idea per sentence while preserving conditions, exceptions, and logical relationships.
26
+ - **Paragraphs:** focus each paragraph on one topic and make its main point clear near the beginning. Remove sentences that do not support that topic or relocate them within the approved scope.
27
+ - **Lists and tables:** use numbered lists when order matters, bullets otherwise, and parallel phrasing for comparable items. Use tables when readers need to compare the same attributes across items, not merely to decorate prose.
28
+ - **Language:** apply these principles idiomatically in the document's language. Do not impose English word order, a passive-voice ban, or sentence-length quotas on Korean or other languages.
29
+
19
30
  ## Workflow
20
31
 
21
- 1. Confirm the target document and requested outcome. Use its matched memory-area metadata when available: `clarify` guidance first, then `description`, `label`, and `role`.
32
+ 1. Confirm the target, audience, requested outcome, and edit scope. Diagnose ambiguity, repetition, or missing information before editing; small changes need no separate diagnostic report. Use matched memory-area metadata when available: `clarify` guidance first, then `description`, `label`, and `role`.
22
33
  2. Read only the context needed to preserve meaning: the nearest README, directly linked documents, relevant query results, and targeted history when a past decision matters.
23
- 3. Clarify purpose, next actions, terms, ownership, headings, links, and runnable examples. Keep exact commands, paths, package names, API names, requirements, and behavior claims unless verified evidence supports a change.
24
- 4. Prefer direct affirmative statements. Use “X is not Y; it is Z” only when that contrast resolves a likely ambiguity. Remove repeated framing, indirect wording, mixed responsibilities, and background that obscures the document's job.
25
- 5. Follow repository-local generated markers and canonical-source instructions. For dotdotgod traceability, edit the fenced `json dotdotgod` block as the canonical mapping and use `dotdotgod traceability links <root> --write` to refresh generated links.
34
+ 3. Apply criteria suited to the document: README navigation and starting points; spec conditions, behavior, and exceptions; test procedures and pass criteria; architecture boundaries, rationale, and constraints; plan ordering, dependencies, and completion criteria; archive decisions and outcomes in their historical context. Repository-specific guidance takes priority. Clarify terms, ownership, headings, links, and runnable examples. Preserve commands, paths, package/API names, requirement strength, conditions, exceptions, unresolved questions, limitations, and evidence unless verified sources support a change. Do not invent missing facts.
35
+ 4. Prefer direct affirmative statements. Remove repeated framing, indirect wording, mixed responsibilities, and irrelevant background. Remove negative or absence statements only when deleting them leaves the reader's understanding, actions, and decisions unchanged. Rewrite redundant contrasts affirmatively: “It is not automatic; the user starts it” becomes “The user starts it.” Preserve meaningful prohibitions, unsupported cases, exceptions, security constraints, and unverified status. Use “X is not Y; it is Z” when the contrast resolves a likely ambiguity. Accuracy takes priority over brevity.
36
+ 5. Keep edits within the agreed scope. Confirm scope before splitting or moving files, changing meaning, or editing additional documents outside that scope. Update the nearest README when approved structural changes affect navigation. Follow generated markers and canonical-source instructions. For dotdotgod traceability, edit the fenced `json dotdotgod` block as the canonical mapping. Inspect `dotdotgod traceability links <root> --check --json` before using `--write`; the root-wide write can change unrelated documents, so obtain approval if its affected files exceed the agreed scope. Apply ordinary reference policy: shared documents must not depend on concrete local file paths in inline code or Markdown links; inline-code directory, glob, and placeholder usage examples remain allowed. Preserve traceability's separate stricter local-target prohibition.
26
37
  6. Ask for a decision when the requested clarification would change established meaning or when current sources conflict.
27
- 7. Run verification that matches the changed surface. Use documentation and traceability checks for ordinary docs; add generation checks, focused package tests, dry-runs, or workspace verification when shared resources or product behavior are affected.
38
+ 7. Run verification that matches the changed surface. Use documentation and traceability checks for ordinary docs; add generation checks, focused package tests, dry-runs, or workspace verification when shared resources or product behavior are affected. Report briefly: what became clearer; important meaning preserved and unresolved questions; checks run and checks not run. Do not equate shorter text with improved accuracy.
28
39
 
29
40
  When CLI-backed routing is unavailable, continue from the target document, nearest README, direct links, and repository conventions.