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