@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.
- package/mcp/cli.mjs +364 -43
- package/mcp/server.mjs +11 -5
- 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 =
|
|
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)
|
|
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
|
|
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
|
|
3227
|
+
let json2 = false;
|
|
3208
3228
|
let confirm = false;
|
|
3209
3229
|
for (const arg of rest) {
|
|
3210
|
-
if (arg === "--json")
|
|
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(
|
|
3343
|
-
if (
|
|
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
|
|
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
|
|
4752
|
-
import { extname as
|
|
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 =
|
|
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 =
|
|
4771
|
-
const absolute =
|
|
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(
|
|
4898
|
+
if (unsafeProfilePath(path) || !TEXT_EXTENSIONS.has(extname6(path).toLowerCase())) return "";
|
|
4801
4899
|
try {
|
|
4802
|
-
const absolute =
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
4988
|
-
const related = report.related.
|
|
4989
|
-
return { changed, related, omittedRelated: Math.max(0, report.related.length -
|
|
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 =
|
|
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
|
|
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 =
|
|
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,
|
|
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:
|
|
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
|
|
5253
|
-
import { join as join14, resolve as
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
28
|
+
"@dotdotgod/context": "0.3.12"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
".claude-plugin",
|