@dotdotgod/claude-code 0.3.10 → 0.3.12

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.
Files changed (3) hide show
  1. package/mcp/cli.mjs +364 -43
  2. package/mcp/server.mjs +11 -5
  3. package/package.json +2 -2
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:
@@ -1711,7 +1718,7 @@ import { spawnSync } from "node:child_process";
1711
1718
  import { basename as basename3, extname as extname2, join as join5 } from "node:path";
1712
1719
 
1713
1720
  // packages/cli/src/index/constants.mjs
1714
- var CACHE_VERSION = 13;
1721
+ var CACHE_VERSION = 15;
1715
1722
  var CACHE_DIR = ".dotdotgod";
1716
1723
  var MANIFEST_FILE = "manifest.json";
1717
1724
 
@@ -2087,8 +2094,16 @@ function addPackageResource(graph, fileId, packagePath, name, target, kind) {
2087
2094
  addNode(graph, id, "package_resource", { name, target, kind, path: packagePath });
2088
2095
  addEdge(graph, fileId, id, "includes_resource", { kind, confidence: "EXTRACTED" });
2089
2096
  }
2097
+ function addReferenceEvidence(graph, sourceId, reference) {
2098
+ const source = graph.nodes.find((node2) => node2.id === sourceId);
2099
+ if (!source) return;
2100
+ source.references ??= [];
2101
+ source.references.push(reference);
2102
+ }
2090
2103
  function addTraceabilityTarget(graph, sourceId, root, relation, targetPath, data = {}, config = defaultMemoryConfig()) {
2091
- if (!isLocalRelativeTraceabilityPath(targetPath) || !existsSync8(resolve5(root, targetPath))) return;
2104
+ if (!isLocalRelativeTraceabilityPath(targetPath)) return;
2105
+ addReferenceEvidence(graph, sourceId, { targetPath, href: targetPath, relation, data: { confidence: "CURATED_TRACEABILITY", ...data } });
2106
+ if (!existsSync8(resolve5(root, targetPath))) return;
2092
2107
  const targetId = `file:${targetPath}`;
2093
2108
  addNode(graph, targetId, "file", fileNodeMetadata(targetPath, null, config));
2094
2109
  addEdge(graph, sourceId, targetId, relation, { confidence: "CURATED_TRACEABILITY", ...data });
@@ -2151,9 +2166,14 @@ function extractMarkdownGraph(root, file, graph, config = defaultMemoryConfig())
2151
2166
  addEdge(graph, fileId, id, "contains_heading", { confidence: "EXTRACTED" });
2152
2167
  }
2153
2168
  for (const { href, line } of extractLinks(content)) {
2154
- const pathPart = href.split("#")[0];
2169
+ const hashIndex = href.indexOf("#");
2170
+ const pathPart = hashIndex < 0 ? href : href.slice(0, hashIndex);
2171
+ const anchor = hashIndex < 0 ? "" : href.slice(hashIndex + 1);
2172
+ const targetPath = pathPart ? rel(root, resolve6(dirname4(file), pathPart)) : path;
2173
+ const data = { line, confidence: "EXTRACTED" };
2174
+ addReferenceEvidence(graph, fileId, { targetPath, href, anchor, relation: "links_to", data });
2175
+ if (pathPart && isReadmeIndexPath(path)) addReferenceEvidence(graph, fileId, { targetPath, href, anchor, relation: "routes_to", data: { line, confidence: "CURATED_INDEX", sourceRole: "readme-index" } });
2155
2176
  if (!pathPart) continue;
2156
- const targetPath = rel(root, resolve6(dirname4(file), pathPart));
2157
2177
  const targetId = `file:${targetPath}`;
2158
2178
  addNode(graph, targetId, "file", fileNodeMetadata(targetPath, null, config));
2159
2179
  addEdge(graph, fileId, targetId, "links_to", { line, confidence: "EXTRACTED" });
@@ -2225,7 +2245,7 @@ function mergeIncrementalGraph(previousGraph, changedGraph, changedPaths) {
2225
2245
  if (!previousGraph) return changedGraph;
2226
2246
  const changedSet = new Set(changedPaths);
2227
2247
  const changedNodeIds = new Set(previousGraph.nodes.filter((node2) => nodeOwnedByPath(node2, changedSet)).map((node2) => node2.id));
2228
- for (const node2 of changedGraph.nodes) changedNodeIds.add(node2.id);
2248
+ for (const node2 of changedGraph.nodes) if (nodeOwnedByPath(node2, changedSet)) changedNodeIds.add(node2.id);
2229
2249
  const graph = { nodes: [], edges: [] };
2230
2250
  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")));
2231
2251
  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")));
@@ -3204,15 +3224,15 @@ import { resolve as resolve11 } from "node:path";
3204
3224
  function parse(argv) {
3205
3225
  const [action2 = "status", ...rest] = argv;
3206
3226
  let root = ".";
3207
- let json = false;
3227
+ let json2 = false;
3208
3228
  let confirm = false;
3209
3229
  for (const arg of rest) {
3210
- if (arg === "--json") json = true;
3230
+ if (arg === "--json") json2 = true;
3211
3231
  else if (arg === "--confirm") confirm = true;
3212
3232
  else if (!arg.startsWith("-") && root === ".") root = arg;
3213
3233
  else usage(`Unknown embedding option: ${arg}`, `embedding ${action2}`);
3214
3234
  }
3215
- return { action: action2, root: resolve11(root), json, confirm };
3235
+ return { action: action2, root: resolve11(root), json: json2, confirm };
3216
3236
  }
3217
3237
  function runEmbedding(argv, options = {}) {
3218
3238
  const parsed = parse(argv);
@@ -3339,8 +3359,8 @@ function buildDocumentationMap(projectRoot = ".", { depth = 5 } = {}) {
3339
3359
  }
3340
3360
 
3341
3361
  // packages/cli/src/commands/map.mjs
3342
- function fail(json, code, message) {
3343
- if (json) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
3362
+ function fail(json2, code, message) {
3363
+ if (json2) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
3344
3364
  else console.error(message);
3345
3365
  process.exit(2);
3346
3366
  }
@@ -3377,7 +3397,7 @@ function runMap(argv) {
3377
3397
  }
3378
3398
 
3379
3399
  // packages/cli/src/reference/resolve.mjs
3380
- import { resolve as resolve15 } from "node:path";
3400
+ import { resolve as resolve16 } from "node:path";
3381
3401
 
3382
3402
  // node_modules/.pnpm/leiden-ts@0.1.0/node_modules/leiden-ts/dist/index.js
3383
3403
  var GraphValidationError = class extends Error {
@@ -4747,16 +4767,94 @@ function compareImpactItems(seeds) {
4747
4767
  };
4748
4768
  }
4749
4769
 
4770
+ // packages/cli/src/impact/references.mjs
4771
+ import { readFileSync as readFileSync13, statSync as statSync8 } from "node:fs";
4772
+ import { extname as extname5, resolve as resolve14 } from "node:path";
4773
+ var WARNING_LIMIT = 20;
4774
+ var edgeKey = (source, target, relation) => `${source}\0${target}\0${relation}`;
4775
+ function prepareImpactReferences(index, root, changedPaths) {
4776
+ const graph = index?.graph ?? { nodes: [], edges: [] };
4777
+ if (!root) return { index, warnings: { items: [], total: 0, omitted: 0 } };
4778
+ const seeds = new Set(changedPaths.map((path) => `file:${path}`));
4779
+ const checks = /* @__PURE__ */ new Map();
4780
+ const inspect = (path) => {
4781
+ if (checks.has(path)) return checks.get(path);
4782
+ let result;
4783
+ try {
4784
+ const absolute = resolve14(root, path);
4785
+ const stat = statSync8(absolute);
4786
+ result = { status: "exists" };
4787
+ if (stat.isFile() && extname5(absolute) === ".md") result.anchors = extractAnchors(readFileSync13(absolute, "utf8"));
4788
+ } catch (error) {
4789
+ result = { status: ["ENOENT", "ENOTDIR"].includes(error.code) ? "missing" : "unavailable" };
4790
+ }
4791
+ checks.set(path, result);
4792
+ return result;
4793
+ };
4794
+ const references = graph.nodes.flatMap((node2) => (node2.references ?? []).map((reference) => ({ ...reference, source: node2.id, sourcePath: node2.path })));
4795
+ const managedEdges = new Set(references.map((ref) => edgeKey(ref.source, `file:${ref.targetPath}`, ref.relation)));
4796
+ const adjacency = /* @__PURE__ */ new Map();
4797
+ const connect = (source, target) => {
4798
+ if (!adjacency.has(source)) adjacency.set(source, /* @__PURE__ */ new Set());
4799
+ if (!adjacency.has(target)) adjacency.set(target, /* @__PURE__ */ new Set());
4800
+ adjacency.get(source).add(target);
4801
+ adjacency.get(target).add(source);
4802
+ };
4803
+ for (const edge of graph.edges) connect(edge.source, edge.target);
4804
+ for (const ref of references) connect(ref.source, `file:${ref.targetPath}`);
4805
+ const reachable = new Set(seeds);
4806
+ const queue = [...seeds];
4807
+ for (let i = 0; i < queue.length; i += 1) for (const id of adjacency.get(queue[i]) ?? []) {
4808
+ if (!reachable.has(id)) {
4809
+ reachable.add(id);
4810
+ queue.push(id);
4811
+ }
4812
+ }
4813
+ const missing = new Set(graph.nodes.filter((node2) => node2.path && inspect(node2.path).status === "missing" && !seeds.has(node2.id)).map((node2) => node2.id));
4814
+ const nodes = new Map(graph.nodes.filter((node2) => !missing.has(node2.id)).map((node2) => [node2.id, node2]));
4815
+ const edges = graph.edges.filter((edge) => !missing.has(edge.source) && !missing.has(edge.target) && !managedEdges.has(edgeKey(edge.source, edge.target, edge.relation)));
4816
+ for (const path of changedPaths) {
4817
+ const id = `file:${path}`;
4818
+ if (!nodes.has(id)) nodes.set(id, { id, type: "file", ...fileNodeMetadata(path, null, index?.memoryConfig) });
4819
+ }
4820
+ const warnings = /* @__PURE__ */ new Map();
4821
+ for (const ref of references) {
4822
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(ref.href)) continue;
4823
+ const targetId = `file:${ref.targetPath}`;
4824
+ const check = inspect(ref.targetPath);
4825
+ let reason = check.status === "missing" ? "MISSING_FILE" : check.status === "unavailable" ? "REFERENCE_UNAVAILABLE" : void 0;
4826
+ if (!reason && ref.anchor && check.anchors) {
4827
+ try {
4828
+ if (!check.anchors.has(decodeURIComponent(ref.anchor))) reason = "BROKEN_ANCHOR";
4829
+ } catch {
4830
+ reason = "BROKEN_ANCHOR";
4831
+ }
4832
+ }
4833
+ if (reason) {
4834
+ if (reachable.has(ref.source) && !missing.has(ref.source)) {
4835
+ const key = `${ref.source}\0${ref.href}\0${ref.data?.line ?? ""}\0${reason}`;
4836
+ if (!warnings.has(key)) warnings.set(key, { source: ref.sourcePath ?? ref.source, target: ref.href, targetPath: ref.targetPath, relation: ref.relation, reason, ...ref.data?.line ? { line: ref.data.line } : {} });
4837
+ }
4838
+ if (reason !== "REFERENCE_UNAVAILABLE" && !(reason === "MISSING_FILE" && seeds.has(targetId))) continue;
4839
+ }
4840
+ if (missing.has(ref.source)) continue;
4841
+ if (!nodes.has(targetId)) nodes.set(targetId, { id: targetId, type: "file", ...fileNodeMetadata(ref.targetPath, null, index?.memoryConfig) });
4842
+ edges.push({ source: ref.source, target: targetId, relation: ref.relation, ...ref.data });
4843
+ }
4844
+ const items = [...warnings.values()].sort((a, b) => a.source.localeCompare(b.source) || (a.line ?? 0) - (b.line ?? 0) || a.target.localeCompare(b.target) || a.reason.localeCompare(b.reason));
4845
+ return { index: { ...index, graph: { nodes: [...nodes.values()], edges: edges.filter((edge) => nodes.has(edge.source) && nodes.has(edge.target)) } }, warnings: { items: items.slice(0, WARNING_LIMIT), total: items.length, omitted: Math.max(0, items.length - WARNING_LIMIT) } };
4846
+ }
4847
+
4750
4848
  // packages/cli/src/impact/vector-profile.mjs
4751
- import { lstatSync, readFileSync as readFileSync13, realpathSync } from "node:fs";
4752
- import { extname as extname5, relative as relative5, resolve as resolve14, sep } from "node:path";
4849
+ import { lstatSync, readFileSync as readFileSync14, realpathSync } from "node:fs";
4850
+ import { extname as extname6, relative as relative5, resolve as resolve15, sep } from "node:path";
4753
4851
  var MAX_VECTOR_PROFILE_FILE_BYTES = 64 * 1024;
4754
4852
  var MAX_VECTOR_PROFILE_CHARS = 4e3;
4755
4853
  var MAX_VECTOR_PROFILE_METADATA_ITEMS = 20;
4756
4854
  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"]);
4757
4855
  var GENERATED_SEGMENTS = /* @__PURE__ */ new Set([".dotdotgod", "build", "coverage", "dist", "node_modules"]);
4758
4856
  function insideRoot(root, absolute) {
4759
- const normalizedRoot = resolve14(root);
4857
+ const normalizedRoot = resolve15(root);
4760
4858
  return absolute === normalizedRoot || absolute.startsWith(`${normalizedRoot}${sep}`);
4761
4859
  }
4762
4860
  function normalizeChangedPath(path) {
@@ -4767,8 +4865,8 @@ function normalizeChangedPath(path) {
4767
4865
  function canonicalizeChangedPath(root, path) {
4768
4866
  const normalized = normalizeChangedPath(path);
4769
4867
  if (!normalized) return null;
4770
- const rootAbsolute = resolve14(root);
4771
- const absolute = resolve14(rootAbsolute, normalized);
4868
+ const rootAbsolute = resolve15(root);
4869
+ const absolute = resolve15(rootAbsolute, normalized);
4772
4870
  if (!insideRoot(rootAbsolute, absolute)) return normalized;
4773
4871
  try {
4774
4872
  const canonicalRoot = realpathSync(rootAbsolute);
@@ -4797,17 +4895,17 @@ function graphMetadata(graph, seedId) {
4797
4895
  return values;
4798
4896
  }
4799
4897
  function safeTextPrefix(root, path) {
4800
- if (unsafeProfilePath(path) || !TEXT_EXTENSIONS.has(extname5(path).toLowerCase())) return "";
4898
+ if (unsafeProfilePath(path) || !TEXT_EXTENSIONS.has(extname6(path).toLowerCase())) return "";
4801
4899
  try {
4802
- const absolute = resolve14(root, path);
4900
+ const absolute = resolve15(root, path);
4803
4901
  if (!insideRoot(root, absolute)) return "";
4804
4902
  const canonicalAbsolute = realpathSync(absolute);
4805
4903
  if (!insideRoot(realpathSync(root), canonicalAbsolute)) return "";
4806
4904
  const canonicalPath = relative5(realpathSync(root), canonicalAbsolute).replaceAll("\\", "/");
4807
- if (canonicalPath !== path || unsafeProfilePath(canonicalPath) || !TEXT_EXTENSIONS.has(extname5(canonicalPath).toLowerCase())) return "";
4905
+ if (canonicalPath !== path || unsafeProfilePath(canonicalPath) || !TEXT_EXTENSIONS.has(extname6(canonicalPath).toLowerCase())) return "";
4808
4906
  const stats = lstatSync(canonicalAbsolute);
4809
4907
  if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_VECTOR_PROFILE_FILE_BYTES) return "";
4810
- const buffer = readFileSync13(canonicalAbsolute);
4908
+ const buffer = readFileSync14(canonicalAbsolute);
4811
4909
  if (buffer.includes(0)) return "";
4812
4910
  const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
4813
4911
  return text.slice(0, MAX_VECTOR_PROFILE_CHARS);
@@ -4923,10 +5021,10 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4923
5021
  const traceabilityWeights = traceabilityRelationWeights(config.traceability);
4924
5022
  policy.relationWeights = { ...policy.relationWeights, ...traceabilityWeights };
4925
5023
  const curatedRelations = new Set(Object.keys(traceabilityWeights));
4926
- const nodeById = new Map(graph.nodes.map((node2) => [node2.id, node2]));
5024
+ const nodeById = new Map(graph.nodes.map(({ references, ...node2 }) => [node2.id, node2]));
4927
5025
  const seeds = changedPaths.map((path) => `file:${path}`);
4928
5026
  const seedSet = new Set(seeds);
4929
- const maxRelated = Math.max(limits.related ?? 25, seeds.length);
5027
+ const maxRelated = Math.max(limits.related ?? 25, 0);
4930
5028
  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 } };
4931
5029
  const relatedIds = new Set(seeds);
4932
5030
  const reasons = new Map(seeds.map((seed) => [seed, /* @__PURE__ */ new Set(["changed-file"])]));
@@ -4962,7 +5060,9 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4962
5060
  const scored = scoreImpactItem({ ...node2, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores, config);
4963
5061
  return { ...node2, reasons: reasonList, hasCuratedEvidence, ...vectorEvidence.has(id) ? { vectorEvidence: vectorEvidence.get(id) } : {}, retrieval: { ...retrieval, signals: [.../* @__PURE__ */ new Set([...retrieval.signals ?? [], ...reasonSignals])] }, ...scored };
4964
5062
  }).sort(compareImpactItems(seeds));
4965
- const related = selectImpactItems(relatedAll, maxRelated, seeds);
5063
+ const excludedIds = new Set(limits.excludeRelatedIds ?? seeds);
5064
+ const rankedRelated = relatedAll.filter((item) => !excludedIds.has(item.id));
5065
+ const related = selectImpactItems(rankedRelated, maxRelated, []);
4966
5066
  for (const item of related) {
4967
5067
  if (item.type === "file") {
4968
5068
  const area2 = docsArea2(item.path, config);
@@ -4975,18 +5075,24 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
4975
5075
  else if (item.type === "event") addImpactItem(groups.events, item, limits.events ?? 10);
4976
5076
  else if (item.type === "package_resource") addImpactItem(groups.packageResources, item, limits.packageResources ?? 10);
4977
5077
  }
4978
- return { changed: changedPaths[0], changedFiles: changedPaths, semantic: { status: overlay.status ?? "disabled", ...limits.verboseSemantic ? { diagnostics: overlay.diagnostics } : {} }, ranking: { method: "weighted-personalized-pagerank+memory", configSource: index?.memoryConfig?.source ?? "default", connectionCap: policy.connectionCap, memoryCap: policy.memoryCap, pprReference: policy.ppr.reference }, related, groups, omittedRelated: Math.max(0, relatedAll.length - related.length) };
5078
+ return { changed: changedPaths[0], changedFiles: changedPaths, semantic: { status: overlay.status ?? "disabled", ...limits.verboseSemantic ? { diagnostics: overlay.diagnostics } : {} }, ranking: { method: "weighted-personalized-pagerank+memory", configSource: index?.memoryConfig?.source ?? "default", connectionCap: policy.connectionCap, memoryCap: policy.memoryCap, pprReference: policy.ppr.reference }, related, groups, omittedRelated: Math.max(0, rankedRelated.length - related.length) };
4979
5079
  }
4980
5080
  function buildImpactReport(index, changedPaths, limits = {}) {
4981
5081
  const normalized = normalizeChangedPaths(changedPaths);
5082
+ const prepared = prepareImpactReferences(index, limits.root, normalized);
5083
+ index = prepared.index;
5084
+ const validIds = new Set(index?.graph?.nodes.map((node2) => node2.id) ?? []);
5085
+ if (limits.overlay) limits = { ...limits, overlay: { ...limits.overlay, edges: (limits.overlay.edges ?? []).filter((edge) => validIds.has(edge.target)) } };
4982
5086
  const aggregate2 = buildCombinedImpactReport(index, normalized, limits);
5087
+ aggregate2.warnings = prepared.warnings;
5088
+ Object.defineProperty(aggregate2, "structuralGraph", { value: index?.graph, enumerable: false });
4983
5089
  const perSeedLimit = limits.perSeed ?? 5;
4984
5090
  aggregate2.perSeed = normalized.map((changed) => {
4985
5091
  const seedId = `file:${changed}`;
4986
5092
  const seedOverlay = limits.overlay ? { ...limits.overlay, edges: (limits.overlay.edges ?? []).filter((edge) => edge.source === seedId) } : void 0;
4987
- const report = buildCombinedImpactReport(index, [changed], { ...limits, overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit + 1) });
4988
- const related = report.related.filter((item) => item.id !== `file:${changed}`).slice(0, perSeedLimit);
4989
- return { changed, related, omittedRelated: Math.max(0, report.related.length - 1 - related.length) + report.omittedRelated };
5093
+ const report = buildCombinedImpactReport(index, [changed], { ...limits, excludeRelatedIds: normalized.map((path) => `file:${path}`), overlay: seedOverlay, related: Math.max(limits.related ?? 25, perSeedLimit) });
5094
+ const related = report.related.slice(0, perSeedLimit);
5095
+ return { changed, related, omittedRelated: Math.max(0, report.related.length - related.length) + report.omittedRelated };
4990
5096
  });
4991
5097
  return aggregate2;
4992
5098
  }
@@ -5006,7 +5112,7 @@ function compactImpactGroup(group = { items: [], omitted: 0 }, limit = 5) {
5006
5112
  return { items, omitted: (group.omitted ?? 0) + Math.max(0, (group.items?.length ?? 0) - items.length) };
5007
5113
  }
5008
5114
  function buildCompactImpactReport(impact, limits = {}) {
5009
- const relatedLimit = Math.max(limits.related ?? 10, impact.changedFiles?.length ?? 1);
5115
+ const relatedLimit = limits.related ?? 10;
5010
5116
  const groupLimit = limits.groupItems ?? 5;
5011
5117
  const changedFiles = impact.changedFiles ?? [impact.changed];
5012
5118
  const seedIds = new Set(changedFiles.map((path) => `file:${path}`));
@@ -5015,11 +5121,11 @@ function buildCompactImpactReport(impact, limits = {}) {
5015
5121
  const groups = Object.fromEntries(groupNames.map((name) => [name, compactImpactGroup(impact.groups?.[name], groupLimit)]));
5016
5122
  const perSeed = (impact.perSeed ?? []).map((entry) => ({ changed: entry.changed, related: (entry.related ?? []).slice(0, limits.perSeed ?? 5).map(compactImpactItem), omittedRelated: entry.omittedRelated ?? 0 }));
5017
5123
  const top10 = (impact.related ?? []).filter((item) => !seedIds.has(item.id)).slice(0, 10);
5018
- return { changed: impact.changed, changedFiles, perSeed, semantic: impact.semantic, compact: true, ranking: { method: impact.ranking?.method, configSource: impact.ranking?.configSource, connectionCap: impact.ranking?.connectionCap, memoryCap: impact.ranking?.memoryCap, pprReference: impact.ranking?.pprReference }, related, groups, omittedRelated: (impact.omittedRelated ?? 0) + Math.max(0, (impact.related?.length ?? 0) - related.length), quality: { rawRelated: impact.related?.length ?? 0, compactRelated: related.length, semanticOnlyTop10: top10.filter((item) => isSemanticOnlyImpactItem(item)).length, curatedTop10: top10.filter((item) => hasCuratedImpactReason(item)).length, lowActionabilityTop10: top10.filter((item) => isLowActionabilityImpactItem(item)).length } };
5124
+ return { changed: impact.changed, changedFiles, perSeed, warnings: impact.warnings, semantic: impact.semantic, compact: true, ranking: { method: impact.ranking?.method, configSource: impact.ranking?.configSource, connectionCap: impact.ranking?.connectionCap, memoryCap: impact.ranking?.memoryCap, pprReference: impact.ranking?.pprReference }, related, groups, omittedRelated: (impact.omittedRelated ?? 0) + Math.max(0, (impact.related?.length ?? 0) - related.length), quality: { rawRelated: impact.related?.length ?? 0, compactRelated: related.length, semanticOnlyTop10: top10.filter((item) => isSemanticOnlyImpactItem(item)).length, curatedTop10: top10.filter((item) => hasCuratedImpactReason(item)).length, lowActionabilityTop10: top10.filter((item) => isLowActionabilityImpactItem(item)).length } };
5019
5125
  }
5020
5126
 
5021
5127
  // packages/cli/src/reference/extract.mjs
5022
- import { basename as basename8, dirname as dirname7, extname as extname6 } from "node:path";
5128
+ import { basename as basename8, dirname as dirname7, extname as extname7 } from "node:path";
5023
5129
  var DEFAULT_REFERENCE_LIMIT = 5;
5024
5130
  function extractBracketReferences(prompt = "") {
5025
5131
  const refs = [];
@@ -5064,7 +5170,7 @@ function isArchiveBodyPath(path = "", memoryConfig) {
5064
5170
  function aliasEntriesForPath(path = "") {
5065
5171
  const entries = [];
5066
5172
  const base = basename8(path);
5067
- const ext = extname6(base);
5173
+ const ext = extname7(base);
5068
5174
  const stem = ext ? base.slice(0, -ext.length) : base;
5069
5175
  const withoutMd = path.replace(/\.md$/i, "");
5070
5176
  const parts = withoutMd.split("/").filter(Boolean);
@@ -5081,7 +5187,7 @@ function referenceCandidateAliases(node2) {
5081
5187
  const entries = [];
5082
5188
  if (node2.type === "file" && path) entries.push(...aliasEntriesForPath(path));
5083
5189
  if (node2.type === "heading") {
5084
- const fileStem = path ? basename8(path, extname6(path)) : "";
5190
+ const fileStem = path ? basename8(path, extname7(path)) : "";
5085
5191
  const anchor = typeof node2.id === "string" && node2.id.includes("#") ? node2.id.split("#").pop() : "";
5086
5192
  for (const alias of [node2.title, anchor, fileStem && node2.title ? `${fileStem}#${node2.title}` : "", fileStem && anchor ? `${fileStem}#${anchor}` : "", path && node2.title ? `${path}#${node2.title}` : "", path && anchor ? `${path}#${anchor}` : ""]) entries.push({ alias, kind: "heading" });
5087
5193
  }
@@ -5200,7 +5306,7 @@ function parseReferenceOptions(argv, command) {
5200
5306
  else filtered.push(arg);
5201
5307
  }
5202
5308
  const operands = filtered.filter((arg) => !arg.startsWith("-"));
5203
- return { ...options, root: resolve15(operands[0] ?? "."), json: filtered.includes("--json"), rootArgv: operands.slice(1) };
5309
+ return { ...options, root: resolve16(operands[0] ?? "."), json: filtered.includes("--json"), rootArgv: operands.slice(1) };
5204
5310
  }
5205
5311
  function formatReferenceOutput(payload) {
5206
5312
  const refreshNote = payload.metadata.cacheRefreshed ? ", refreshed" : "";
@@ -5240,8 +5346,8 @@ function runExpand(argv) {
5240
5346
  if (options.withImpact) refs = refs.map((item) => {
5241
5347
  const topPath = item.top?.path;
5242
5348
  if (!topPath) return item;
5243
- const impact = buildCompactImpactReport(buildImpactReport(index, topPath));
5244
- return { ...item, impact: { changed: topPath, related: impact.related, groups: impact.groups, omittedRelated: impact.omittedRelated, quality: impact.quality } };
5349
+ const impact = buildCompactImpactReport(buildImpactReport(index, topPath, { root: options.root }));
5350
+ return { ...item, impact: { changed: topPath, warnings: impact.warnings, related: impact.related, groups: impact.groups, omittedRelated: impact.omittedRelated, quality: impact.quality } };
5245
5351
  });
5246
5352
  const payload = { ok: status.ok, command: "expand", root: options.root, prompt, status, metadata, refs, omitted: refs.reduce((sum, item) => sum + item.omitted, 0) };
5247
5353
  if (options.json) console.log(JSON.stringify(payload, null, 2));
@@ -5249,8 +5355,8 @@ function runExpand(argv) {
5249
5355
  }
5250
5356
 
5251
5357
  // packages/cli/src/commands/traceability.mjs
5252
- import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
5253
- import { join as join14, resolve as resolve16 } from "node:path";
5358
+ import { existsSync as existsSync15, readFileSync as readFileSync15, readdirSync as readdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
5359
+ import { join as join14, resolve as resolve17 } from "node:path";
5254
5360
  function collectDocsMarkdownFiles(root) {
5255
5361
  const docs = join14(root, "docs");
5256
5362
  const files = [];
@@ -5282,7 +5388,7 @@ function parseTraceabilityOptions(argv) {
5282
5388
  }
5283
5389
  if (options.check && options.write) usage("Choose only one traceability links mode: --check or --write.", "traceability links");
5284
5390
  options.check = options.check || !options.write;
5285
- options.root = resolve16(options.root);
5391
+ options.root = resolve17(options.root);
5286
5392
  return options;
5287
5393
  }
5288
5394
  function runTraceability(argv) {
@@ -5293,7 +5399,7 @@ function runTraceability(argv) {
5293
5399
  const errors = [...config.errors ?? []];
5294
5400
  for (const file of files) {
5295
5401
  if (options.write && errors.length > 0) break;
5296
- const content = readFileSync14(file, "utf8");
5402
+ const content = readFileSync15(file, "utf8");
5297
5403
  for (const error of validateTraceabilityLinksRegion(content, options.root, file)) errors.push(error);
5298
5404
  const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
5299
5405
  if (blocks.length === 0) continue;
@@ -5321,7 +5427,195 @@ function runTraceability(argv) {
5321
5427
  process.exit(ok ? 0 : 1);
5322
5428
  }
5323
5429
 
5430
+ // packages/cli/src/graph-view/server.mjs
5431
+ import { createServer } from "node:http";
5432
+ import { readFileSync as readFileSync16 } from "node:fs";
5433
+ import { dirname as dirname8, extname as extname8, join as join15 } from "node:path";
5434
+ import { fileURLToPath } from "node:url";
5435
+
5436
+ // packages/cli/src/graph-view/payload.mjs
5437
+ function edgeKey2(edge) {
5438
+ return `${edge.source}\0${edge.target}\0${edge.relation}`;
5439
+ }
5440
+ function labelForNode(node2) {
5441
+ if (node2.type === "contract") return node2.title ?? node2.contractId ?? node2.id;
5442
+ return node2.path ?? node2.command ?? node2.name ?? node2.target ?? node2.id;
5443
+ }
5444
+ function adjacencyFor(nodes, edges) {
5445
+ const adjacency = new Map(nodes.map((node2) => [node2.id, /* @__PURE__ */ new Set()]));
5446
+ for (const edge of edges) {
5447
+ adjacency.get(edge.source)?.add(edge.target);
5448
+ adjacency.get(edge.target)?.add(edge.source);
5449
+ }
5450
+ return adjacency;
5451
+ }
5452
+ function rootReachability(nodes, edges, seedIds) {
5453
+ const adjacency = adjacencyFor(nodes, edges);
5454
+ const reachable = /* @__PURE__ */ new Set();
5455
+ const depthByNode = /* @__PURE__ */ new Map();
5456
+ const nearestRootByNode = /* @__PURE__ */ new Map();
5457
+ const queue = [];
5458
+ for (const node2 of nodes) {
5459
+ if (!seedIds.has(node2.id)) continue;
5460
+ reachable.add(node2.id);
5461
+ depthByNode.set(node2.id, 0);
5462
+ nearestRootByNode.set(node2.id, node2.id);
5463
+ queue.push(node2.id);
5464
+ }
5465
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
5466
+ const current = queue[cursor];
5467
+ for (const neighbor of adjacency.get(current) ?? []) {
5468
+ if (reachable.has(neighbor)) continue;
5469
+ reachable.add(neighbor);
5470
+ depthByNode.set(neighbor, (depthByNode.get(current) ?? 0) + 1);
5471
+ nearestRootByNode.set(neighbor, nearestRootByNode.get(current));
5472
+ queue.push(neighbor);
5473
+ }
5474
+ }
5475
+ return { reachable, depthByNode, nearestRootByNode };
5476
+ }
5477
+ function buildImpactGraphPayload(index, impact) {
5478
+ const graph = impact.structuralGraph ?? index?.graph ?? { nodes: [], edges: [] };
5479
+ const config = index?.memoryConfig;
5480
+ const seedIds = new Set((impact.changedFiles ?? [impact.changed]).map((path) => `file:${path}`));
5481
+ const impactItems = new Map((impact.related ?? []).map((item) => [item.id, item]));
5482
+ const isAllowedNode = (node2) => {
5483
+ if (!node2) return false;
5484
+ if (node2.path && resolveMemoryArea(node2.path, config)?.scope === "local") return false;
5485
+ return true;
5486
+ };
5487
+ const sourceNodes = (graph.nodes ?? []).filter(isAllowedNode);
5488
+ const indexedNodes = new Map(sourceNodes.map((node2) => [node2.id, node2]));
5489
+ const seenEdges = /* @__PURE__ */ new Set();
5490
+ const edges = [];
5491
+ for (const edge of graph.edges ?? []) {
5492
+ if (!indexedNodes.has(edge.source) || !indexedNodes.has(edge.target)) continue;
5493
+ const key = edgeKey2(edge);
5494
+ if (seenEdges.has(key)) continue;
5495
+ seenEdges.add(key);
5496
+ 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 });
5497
+ }
5498
+ const { reachable, depthByNode, nearestRootByNode } = rootReachability(sourceNodes, edges, seedIds);
5499
+ const retainedNodes = sourceNodes.filter((node2) => reachable.has(node2.id));
5500
+ const retainedEdges = edges.filter((edge) => reachable.has(edge.source) && reachable.has(edge.target));
5501
+ const nodes = retainedNodes.map((source) => {
5502
+ const item = impactItems.get(source.id);
5503
+ const seed = seedIds.has(source.id);
5504
+ return {
5505
+ id: source.id,
5506
+ type: source.type ?? item?.type ?? "file",
5507
+ path: source.path ?? item?.path,
5508
+ label: labelForNode({ ...source, ...item }),
5509
+ seed,
5510
+ ranked: Boolean(item),
5511
+ depth: depthByNode.get(source.id) ?? 0,
5512
+ nearestRoot: nearestRootByNode.get(source.id),
5513
+ component: 0,
5514
+ seededComponent: true,
5515
+ impactScore: seed ? 100 : item?.impactScore ?? 0,
5516
+ reasons: seed ? ["changed-file"] : item?.reasons ?? [],
5517
+ scoreBreakdown: seed ? { seed: 100 } : item?.scoreBreakdown,
5518
+ retrieval: item?.retrieval ?? source.retrieval,
5519
+ contractId: item?.contractId ?? source.contractId,
5520
+ title: item?.title ?? source.title,
5521
+ sections: item?.sections ?? source.sections,
5522
+ vectorEvidence: item?.vectorEvidence
5523
+ };
5524
+ });
5525
+ return {
5526
+ changed: impact.changed,
5527
+ changedFiles: impact.changedFiles ?? [impact.changed],
5528
+ ranking: impact.ranking,
5529
+ semantic: impact.semantic,
5530
+ complete: true,
5531
+ nodes,
5532
+ edges: retainedEdges,
5533
+ components: retainedNodes.length > 0 ? [{ id: 0, size: retainedNodes.length, seeded: true }] : [],
5534
+ diagnostics: {
5535
+ rootNodes: retainedNodes.filter((node2) => seedIds.has(node2.id)).length,
5536
+ connectedNodes: retainedNodes.length,
5537
+ disconnectedNodesOmitted: sourceNodes.length - retainedNodes.length,
5538
+ maximumDepth: Math.max(0, ...depthByNode.values())
5539
+ },
5540
+ omittedRelated: impact.omittedRelated ?? 0
5541
+ };
5542
+ }
5543
+
5544
+ // packages/cli/src/graph-view/server.mjs
5545
+ var ASSET_ROOT = join15(dirname8(fileURLToPath(import.meta.url)), "assets");
5546
+ 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" };
5547
+ var VENDOR_FILES = {
5548
+ "/vendor/sigma.js": new URL("../../node_modules/sigma/dist/sigma.min.js", import.meta.url),
5549
+ "/vendor/graphology.js": new URL("../../node_modules/graphology/dist/graphology.umd.min.js", import.meta.url),
5550
+ "/vendor/d3.js": new URL("../../node_modules/d3/dist/d3.min.js", import.meta.url)
5551
+ };
5552
+ function json(response, status, value) {
5553
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
5554
+ response.end(JSON.stringify(value));
5555
+ }
5556
+ function asset(response, file) {
5557
+ try {
5558
+ const body = readFileSync16(file);
5559
+ response.writeHead(200, { "content-type": CONTENT_TYPES[extname8(file)] ?? "application/octet-stream", "cache-control": "no-store" });
5560
+ response.end(body);
5561
+ } catch {
5562
+ response.writeHead(404);
5563
+ response.end("Not found");
5564
+ }
5565
+ }
5566
+ async function buildServedImpact(root, changedPaths) {
5567
+ const normalized = [...new Set(changedPaths.map((path) => canonicalizeChangedPath(root, path)).filter(Boolean))];
5568
+ const { status, index, metadata } = readFreshIndex(root);
5569
+ const overlay = await buildVectorImpactOverlay(root, index, normalized);
5570
+ const impact = buildImpactReport(index, normalized, { root, overlay, verboseSemantic: false, related: 40 });
5571
+ const graph = buildImpactGraphPayload(index, impact);
5572
+ const graphIds = new Set(graph.nodes.map((node2) => node2.id));
5573
+ const explorerImpact = {
5574
+ ...impact,
5575
+ related: impact.related.filter((item) => graphIds.has(item.id))
5576
+ };
5577
+ return { ok: status.ok, status, metadata, impact: explorerImpact, graph };
5578
+ }
5579
+ async function startImpactGraphServer({ root, changed, host = "127.0.0.1", port = 0 }) {
5580
+ let payload = await buildServedImpact(root, changed);
5581
+ const server = createServer(async (request, response) => {
5582
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? host}`);
5583
+ if (url.pathname === "/api/impact") return json(response, 200, payload);
5584
+ if (url.pathname === "/api/reroot") {
5585
+ const paths = url.searchParams.getAll("changed");
5586
+ if (paths.length === 0) return json(response, 400, { ok: false, error: { code: "MISSING_CHANGED", message: "At least one changed path is required." } });
5587
+ try {
5588
+ payload = await buildServedImpact(root, paths);
5589
+ return json(response, 200, payload);
5590
+ } catch (error) {
5591
+ return json(response, 500, { ok: false, error: { code: "GRAPH_SERVE_FAILED", message: error instanceof Error ? error.message : String(error) } });
5592
+ }
5593
+ }
5594
+ if (VENDOR_FILES[url.pathname]) return asset(response, fileURLToPath(VENDOR_FILES[url.pathname]));
5595
+ const relative6 = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
5596
+ if (!["index.html", "app.css", "app.js"].includes(relative6)) {
5597
+ response.writeHead(404);
5598
+ return response.end("Not found");
5599
+ }
5600
+ return asset(response, join15(ASSET_ROOT, relative6));
5601
+ });
5602
+ await new Promise((resolve18, reject) => {
5603
+ server.once("error", reject);
5604
+ server.listen(port, host, resolve18);
5605
+ });
5606
+ const address = server.address();
5607
+ const actualPort = typeof address === "object" && address ? address.port : port;
5608
+ return { server, url: `http://${host}:${actualPort}`, payload };
5609
+ }
5610
+
5324
5611
  // packages/cli/src/impact/format.mjs
5612
+ function formatImpactWarnings(warnings) {
5613
+ if (!warnings?.total) return [];
5614
+ return [
5615
+ `Reference warnings: ${warnings.total} (${warnings.omitted} omitted); broken references excluded from impact.`,
5616
+ ...warnings.items.map((item) => `- ${item.source}${item.line ? `:${item.line}` : ""} -> ${item.target} [${item.reason}]${item.reason === "REFERENCE_UNAVAILABLE" ? " (access uncertain; not excluded)" : ""}`)
5617
+ ];
5618
+ }
5325
5619
  function formatCompactImpactGroup(name, group) {
5326
5620
  const items = group?.items ?? [];
5327
5621
  if (items.length === 0) return [];
@@ -5337,6 +5631,7 @@ function formatCompactImpactGroup(name, group) {
5337
5631
  function formatCompactImpactOutput(payload, impact) {
5338
5632
  const refreshNote = payload.metadata.cacheRefreshed ? ", refreshed" : "";
5339
5633
  const lines = [`graph impact compact: ${impact.related.length} related node(s), ${impact.omittedRelated ?? 0} omitted (${payload.status.status}${refreshNote} index)`, `changed files: ${(impact.changedFiles ?? [impact.changed]).join(", ")}`];
5634
+ lines.push(...formatImpactWarnings(impact.warnings));
5340
5635
  for (const entry of impact.perSeed ?? []) lines.push(...formatCompactImpactGroup(`top for ${entry.changed}`, { items: entry.related }));
5341
5636
  for (const name of ["docs", "contracts", "tests", "files", "commands", "events", "packageResources", "symbols"]) lines.push(...formatCompactImpactGroup(name, impact.groups[name]));
5342
5637
  return lines.join("\n");
@@ -5400,6 +5695,13 @@ function formatYmlImpactOutput(payload, impact) {
5400
5695
  }
5401
5696
  lines.push(" groups:");
5402
5697
  for (const name of ["docs", "contracts", "tests", "files", "commands", "events", "packageResources", "symbols"]) lines.push(...formatYmlImpactGroup(name, impact.groups[name]));
5698
+ lines.push(" warnings:");
5699
+ lines.push(` total: ${impact.warnings?.total ?? 0}`, ` omitted: ${impact.warnings?.omitted ?? 0}`, " items:");
5700
+ if (!impact.warnings?.items?.length) lines.push(" []");
5701
+ else for (const item of impact.warnings.items) {
5702
+ lines.push(` - source: ${ymlScalar(item.source)}`, ` target: ${ymlScalar(item.target)}`, ` target_path: ${ymlScalar(item.targetPath)}`, ` relation: ${ymlScalar(item.relation)}`, ` reason: ${ymlScalar(item.reason)}`);
5703
+ if (item.line) lines.push(` line: ${item.line}`);
5704
+ }
5403
5705
  lines.push(" recommended_actions:");
5404
5706
  lines.push(' - "review_related_docs"');
5405
5707
  lines.push(' - "run_related_tests"');
@@ -5428,6 +5730,8 @@ function parseGraphOptions(argv) {
5428
5730
  const changed = [];
5429
5731
  let compact = false;
5430
5732
  let yml = false;
5733
+ let host = "127.0.0.1";
5734
+ let port = 0;
5431
5735
  for (let i = 0; i < argv.length; i += 1) {
5432
5736
  if (argv[i] === "--changed") {
5433
5737
  const next = argv[i + 1];
@@ -5437,19 +5741,36 @@ function parseGraphOptions(argv) {
5437
5741
  }
5438
5742
  } else if (argv[i] === "--compact") compact = true;
5439
5743
  else if (argv[i] === "--yml" || argv[i] === "--yaml") yml = true;
5440
- else filtered.push(argv[i]);
5744
+ else if (argv[i] === "--host" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
5745
+ host = argv[i + 1];
5746
+ i += 1;
5747
+ } else if (argv[i] === "--port" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
5748
+ port = Number(argv[i + 1]);
5749
+ i += 1;
5750
+ } else filtered.push(argv[i]);
5441
5751
  }
5442
5752
  const options = parseCommon(filtered);
5443
5753
  options.changed = [...new Set(changed)];
5444
5754
  options.compact = compact;
5445
5755
  options.yml = yml;
5756
+ options.host = host;
5757
+ options.port = Number.isInteger(port) && port >= 0 && port <= 65535 ? port : NaN;
5446
5758
  return options;
5447
5759
  }
5448
5760
  async function runGraph(argv) {
5449
5761
  const sub = argv[0];
5450
5762
  const isImpact = sub === "impact";
5451
- if (!["impact", "communities"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
5763
+ const isServe = sub === "serve";
5764
+ if (!["impact", "communities", "serve"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
5452
5765
  const options = parseGraphOptions(argv.slice(1));
5766
+ if (isServe && options.changed.length === 0) usage("Missing required option: --changed <path>.", "graph serve");
5767
+ if (isServe && !Number.isInteger(options.port)) usage("Invalid --port. Use an integer from 0 through 65535.", "graph serve");
5768
+ if (isServe) {
5769
+ options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
5770
+ const running = await startImpactGraphServer({ root: options.root, changed: options.changed, host: options.host, port: options.port });
5771
+ console.log(`graph serve: ${running.url}`);
5772
+ return;
5773
+ }
5453
5774
  if (isImpact && [options.json, options.compact, options.yml].filter(Boolean).length > 1) {
5454
5775
  const message = "Choose only one graph impact output mode: --compact, --json, or --yml/--yaml.";
5455
5776
  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));
@@ -5484,14 +5805,14 @@ async function runGraph(argv) {
5484
5805
  if (isImpact) options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
5485
5806
  const { status, index, metadata } = readFreshIndex(options.root);
5486
5807
  const overlay = isImpact ? await buildVectorImpactOverlay(options.root, index, options.changed) : void 0;
5487
- const rawImpact = isImpact ? buildImpactReport(index, options.changed, { overlay, verboseSemantic: options.json }) : void 0;
5808
+ const rawImpact = isImpact ? buildImpactReport(index, options.changed, { root: options.root, overlay, verboseSemantic: options.json }) : void 0;
5488
5809
  const impact = isImpact && (options.compact || options.yml) ? buildCompactImpactReport(rawImpact) : rawImpact;
5489
5810
  const payload = isImpact ? { ok: status.ok, command: "graph impact", compact: options.compact || void 0, root: options.root, status, metadata, changed: options.changed[0], changedFiles: options.changed, related: impact.related, impact } : { ok: status.ok, command: "graph communities", root: options.root, status, metadata, graph: graphSummary(index), communities: buildCommunities(index) };
5490
5811
  const refreshNote = metadata.cacheRefreshed ? ", refreshed" : "";
5491
5812
  if (options.json) console.log(JSON.stringify(payload, null, 2));
5492
5813
  else if (isImpact && options.yml) console.log(formatYmlImpactOutput(payload, impact));
5493
5814
  else if (isImpact && options.compact) console.log(formatCompactImpactOutput(payload, impact));
5494
- else if (isImpact) console.log(`graph impact: ${payload.related.length} related node(s), ${impact.omittedRelated ?? 0} omitted (${status.status}${refreshNote} index)`);
5815
+ else if (isImpact) console.log([`graph impact: ${payload.related.length} related node(s), ${impact.omittedRelated ?? 0} omitted (${status.status}${refreshNote} index)`, ...formatImpactWarnings(impact.warnings)].join("\n"));
5495
5816
  else console.log(`graph communities: ${payload.communities.communities.length}/${payload.communities.total} shown, ${payload.communities.omitted} omitted (${status.status}${refreshNote} index)`);
5496
5817
  }
5497
5818
 
package/mcp/server.mjs CHANGED
@@ -21745,7 +21745,13 @@ var ContextStore = class {
21745
21745
  }
21746
21746
  const filterSql = filters.length ? ` AND ${filters.join(" AND ")}` : "";
21747
21747
  const select = "SELECT c.source_id, c.ordinal, c.body, s.label, s.kind, s.scope, s.metadata";
21748
- const porterRows = this.db.prepare(`
21748
+ const wildcard = String(query ?? "").trim() === "*";
21749
+ const porterRows = wildcard ? this.db.prepare(`
21750
+ ${select}, 0 AS rank
21751
+ FROM chunks c JOIN sources s ON s.id = c.source_id
21752
+ WHERE 1 = 1${filterSql}
21753
+ ORDER BY s.created_at DESC, s.id, c.ordinal LIMIT ?
21754
+ `).all(...filterParams, candidateLimit) : this.db.prepare(`
21749
21755
  ${select}, bm25(chunks) AS rank
21750
21756
  FROM chunks c JOIN sources s ON s.id = c.source_id
21751
21757
  WHERE chunks MATCH ?${filterSql}
@@ -21764,7 +21770,7 @@ var ContextStore = class {
21764
21770
  `).all(...labelParams, ...filterParams, candidateLimit);
21765
21771
  const typoWindow = Math.min(500, candidateLimit * 5);
21766
21772
  const typoHalf = Math.max(1, Math.floor(typoWindow / 2));
21767
- const trigramRows = this.db.prepare(`
21773
+ const trigramRows = wildcard ? [] : this.db.prepare(`
21768
21774
  SELECT * FROM (
21769
21775
  SELECT ${select.replace("SELECT ", "")}, 0 AS rank
21770
21776
  FROM chunks c JOIN sources s ON s.id = c.source_id
@@ -21788,7 +21794,7 @@ var ContextStore = class {
21788
21794
  return { sourceId: row.source_id, ordinal: Number(row.ordinal), body: row.body, label: row.label, kind: row.kind, scope: row.scope, rank: row.rank, metadata: readProvenanceMetadata(storedMetadata) };
21789
21795
  };
21790
21796
  const fused = reciprocalRankFusion([
21791
- { name: "porter-bm25", candidates: porterRows.map(toCandidate) },
21797
+ { name: wildcard ? "bounded-browse" : "porter-bm25", candidates: porterRows.map(toCandidate) },
21792
21798
  { name: "label-path", candidates: labelRows.map(toCandidate) },
21793
21799
  { name: "trigram-v1", candidates: trigramRows.map(toCandidate) }
21794
21800
  ], { limit: candidateLimit });
@@ -21801,7 +21807,7 @@ var ContextStore = class {
21801
21807
  scope: row.scope,
21802
21808
  ordinal: row.ordinal,
21803
21809
  rank: row.rank,
21804
- text: excerpt(row.body, query),
21810
+ text: excerpt(row.body, wildcard ? "" : query),
21805
21811
  metadata: row.metadata,
21806
21812
  trust: row.metadata.trust,
21807
21813
  sourceType: row.metadata.sourceType,
@@ -23426,7 +23432,7 @@ register("index", "Index a local text file or bounded directory into the project
23426
23432
  maxFiles: external_exports.number().int().nonnegative().optional(),
23427
23433
  maxAggregateBytes: external_exports.number().int().nonnegative().optional()
23428
23434
  }, (input, extra) => ({ ok: true, ...indexFile(getStore(), { ...input, root }, sessionId, extra.signal) }), { readOnlyHint: true });
23429
- register("search", "Search indexed command, file, and fetched content and return bounded excerpts.", {
23435
+ register("search", "Search indexed content for bounded excerpts. For failure diagnostics try fail OR error OR reason. Query * browses bounded excerpts with the same source/scope/session filters and limit; neither no matches nor partial excerpts prove diagnostics are absent.", {
23430
23436
  query: external_exports.string().min(1),
23431
23437
  scope: scopeSchema,
23432
23438
  source: external_exports.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/claude-code",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
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.10"
28
+ "@dotdotgod/context": "0.3.12"
29
29
  },
30
30
  "files": [
31
31
  ".claude-plugin",