@dotdotgod/claude-code 0.6.6 → 0.7.0
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/commands/dd/init.md +1 -1
- package/mcp/cli.mjs +402 -385
- package/mcp/server.mjs +2 -4
- package/package.json +2 -2
- package/skills/project-initializer/scripts/init_project.sh +22 -130
- package/skills/project-initializer/templates/case-and-evidence.map.md +3 -0
- package/skills/project-initializer/templates/policy.map.md +4 -0
- package/skills/project-initializer/templates/portfolio.map.md +4 -0
- package/skills/project-initializer/templates/publication.map.md +4 -0
- package/skills/project-initializer/templates/research.map.md +6 -0
- package/skills/project-initializer/templates/software.map.md +3 -0
package/mcp/cli.mjs
CHANGED
|
@@ -760,6 +760,24 @@ var init_parser = __esm({
|
|
|
760
760
|
}
|
|
761
761
|
});
|
|
762
762
|
|
|
763
|
+
// packages/cli/src/graph-view/node-style.cjs
|
|
764
|
+
var require_node_style = __commonJS({
|
|
765
|
+
"packages/cli/src/graph-view/node-style.cjs"(exports, module) {
|
|
766
|
+
var palette = ["#b9a3ff", "#70c9a2", "#72b9e6", "#d6b879", "#e393a9", "#78c9cd", "#a6c67a", "#e5a475"];
|
|
767
|
+
function describeNode(path, root = "docs") {
|
|
768
|
+
root = root.replace(/^\.\//, "").replace(/\/$/, "");
|
|
769
|
+
if (!path.startsWith(`${root}/`)) return { label: path, group: null, color: null };
|
|
770
|
+
const parts = path.slice(root.length + 1).split("/");
|
|
771
|
+
const group = parts.length === 1 ? root : parts.shift();
|
|
772
|
+
let hash2 = 0;
|
|
773
|
+
for (const char of group) hash2 = Math.imul(hash2, 31) + char.charCodeAt(0) >>> 0;
|
|
774
|
+
const known = { docs: "#b9a3ff", spec: "#72b9e6", arch: "#d6b879", test: "#70c9a2", concept: "#e393a9", report: "#78c9cd" };
|
|
775
|
+
return { label: parts.join("/"), group, color: known[group] ?? palette[hash2 % palette.length] };
|
|
776
|
+
}
|
|
777
|
+
module.exports = { describeNode };
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
763
781
|
// packages/cli/src/init.mjs
|
|
764
782
|
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
765
783
|
import { basename, dirname as dirname2, join as join5, resolve as resolve3 } from "node:path";
|
|
@@ -1503,9 +1521,15 @@ function memoryConfigSummary(config) {
|
|
|
1503
1521
|
|
|
1504
1522
|
// packages/cli/src/config/templates.mjs
|
|
1505
1523
|
var DEFAULT_TEMPLATE_NAME = "software";
|
|
1524
|
+
var engineeringScaffold = [
|
|
1525
|
+
["docs/spec/README.md", "Specs", "Use this area for behavior specs, API contracts, and requirements. Follow `dotdotgod validate` traceability guidance for applicable documents."],
|
|
1526
|
+
["docs/arch/README.md", "Architecture", "Use this area for architecture decisions, design rationale, conventions, and integration boundaries."],
|
|
1527
|
+
["docs/test/README.md", "Verification", "Use this area for test strategy, evaluation methods, regression cases, and verification records."]
|
|
1528
|
+
];
|
|
1506
1529
|
var BUILT_IN_TEMPLATE_SCAFFOLDS = {
|
|
1507
|
-
software:
|
|
1530
|
+
software: engineeringScaffold,
|
|
1508
1531
|
research: [
|
|
1532
|
+
...engineeringScaffold,
|
|
1509
1533
|
["docs/research/README.md", "Research", "Use this area for research notes, sources, and findings."],
|
|
1510
1534
|
["docs/record/README.md", "Research Records", "Use this area for dated measurements, experiments, and execution records."],
|
|
1511
1535
|
["docs/report/README.md", "Reports", "Use this area for research diagnoses, analyses, results, and performance reports."],
|
|
@@ -1771,11 +1795,10 @@ function formatAction(item) {
|
|
|
1771
1795
|
return `${item.status.padEnd(13)} ${item.path}${extra}`;
|
|
1772
1796
|
}
|
|
1773
1797
|
function parseInitOptions(argv) {
|
|
1774
|
-
const options = { root: null, projectName: "",
|
|
1798
|
+
const options = { root: null, projectName: "", dryRun: false, json: false, template: null, documentationRoot: "docs", documentationRootExplicit: false };
|
|
1775
1799
|
for (let i = 0; i < argv.length; i += 1) {
|
|
1776
1800
|
const arg = argv[i];
|
|
1777
1801
|
if (arg === "--json") options.json = true;
|
|
1778
|
-
else if (arg === "--dotdot-setting") options.dotdotSetting = true;
|
|
1779
1802
|
else if (arg === "--dry-run") options.dryRun = true;
|
|
1780
1803
|
else if (arg === "--claude-plans") options.claudePlans = true;
|
|
1781
1804
|
else if (arg === "--project-name" || arg === "--template" || arg === "--documentation-root") {
|
|
@@ -1854,8 +1877,7 @@ function ensureGitignoreEntry(options, entry, actions) {
|
|
|
1854
1877
|
`);
|
|
1855
1878
|
actions.push(action(existed ? "updated" : "created", target, { add: entry }));
|
|
1856
1879
|
}
|
|
1857
|
-
function agentContent(projectName,
|
|
1858
|
-
const dotdotAgentRule = dotdotSetting ? "\n- Follow the project documentation structure in `docs/arch/DOCS_STRUCTURE.md` and code conventions in `docs/arch/CODE_CONVENTIONS.md`." : "";
|
|
1880
|
+
function agentContent(projectName, domainMap) {
|
|
1859
1881
|
return `# AGENTS.md
|
|
1860
1882
|
|
|
1861
1883
|
Canonical instructions for AI coding agents working in this repository.
|
|
@@ -1873,7 +1895,7 @@ Canonical instructions for AI coding agents working in this repository.
|
|
|
1873
1895
|
- Preserve user edits and unrelated dirty worktree changes.
|
|
1874
1896
|
- Prefer existing local patterns over introducing new abstractions.
|
|
1875
1897
|
- Update docs when behavior, architecture, or test strategy changes.
|
|
1876
|
-
- When using the dotdotgod CLI, run \`dotdotgod validate\` after docs changes and follow its traceability guidance
|
|
1898
|
+
- When using the dotdotgod CLI, run \`dotdotgod validate\` after docs changes and follow its traceability guidance.
|
|
1877
1899
|
|
|
1878
1900
|
## dotdotgod
|
|
1879
1901
|
|
|
@@ -1898,15 +1920,12 @@ TODO
|
|
|
1898
1920
|
|
|
1899
1921
|
## Documentation Map
|
|
1900
1922
|
|
|
1901
|
-
|
|
1902
|
-
- \`docs/test/\`: test strategy, regression cases, manual verification notes.
|
|
1903
|
-
- \`docs/arch/\`: architecture decisions, code conventions, module boundaries, data flow, infrastructure/runtime dependencies, integration boundaries, and migration design.
|
|
1923
|
+
${domainMap}
|
|
1904
1924
|
- \`docs/\`: all directories use kebab-case; all markdown file names use UPPER_SNAKE_CASE, including \`README.md\`.
|
|
1905
1925
|
- \`docs/\`: prefer keeping individual markdown files under the configured markdown validation budgets (default 200 lines and 10,000 characters); split larger docs into focused UPPER_SNAKE_CASE files and keep \`README.md\` as the index/overview unless a narrow size-check exception is configured.
|
|
1906
1926
|
- \`docs/\`: when adding, renaming, splitting, moving, or archiving docs, update the nearest relevant \`README.md\` index/table of contents in the same change.
|
|
1907
1927
|
- \`docs/\`: each docs subdirectory \`README.md\` acts as the local table of contents; list important files, task directories, status, and a one-line purpose for each entry.
|
|
1908
1928
|
- \`docs/\`: start small with a single focused markdown file; when one domain grows into multiple docs, promote it to \`docs/<area>/<domain>/README.md\` plus related UPPER_SNAKE_CASE files in that directory.
|
|
1909
|
-
- \`docs/arch/\`: code conventions may start as \`CODE_CONVENTIONS.md\`; when they grow across multiple topics, use \`docs/arch/conventions/README.md\` as the index with supporting UPPER_SNAKE_CASE files.
|
|
1910
1929
|
- \`docs/plan/\`: local active implementation plans. Create one kebab-case directory per task (\`docs/plan/<task-slug>/\`), keep the task overview/index in that directory's \`README.md\`, and add supporting UPPER_SNAKE_CASE plan files alongside it. Ignored by git by default.
|
|
1911
1930
|
- \`docs/archive/\`: local completed plans, temporary reports, historical notes, payload captures. Move completed plan task directories to \`docs/archive/plan/<task-slug>/\`; put temporary reports and investigations under \`docs/archive/report/<report-slug>/\`. Ignored by git by default.
|
|
1912
1931
|
|
|
@@ -1917,7 +1936,7 @@ TODO
|
|
|
1917
1936
|
|
|
1918
1937
|
Keep long-lived instructions here so agent-specific files do not drift.`;
|
|
1919
1938
|
}
|
|
1920
|
-
function docsReadmeContent() {
|
|
1939
|
+
function docsReadmeContent(domainMap) {
|
|
1921
1940
|
return `# Docs
|
|
1922
1941
|
|
|
1923
1942
|
This directory keeps project knowledge close to the code.
|
|
@@ -1936,114 +1955,20 @@ This directory keeps project knowledge close to the code.
|
|
|
1936
1955
|
|
|
1937
1956
|
## Map
|
|
1938
1957
|
|
|
1939
|
-
|
|
1940
|
-
- \`test/\`: test strategy, regression cases, manual verification notes.
|
|
1941
|
-
- \`arch/\`: architecture decisions, code conventions, module boundaries, data flow, infrastructure/runtime dependencies, integration boundaries, and migration design.
|
|
1958
|
+
${domainMap.replaceAll("`docs/", "`")}
|
|
1942
1959
|
- \`plan/\`: local active implementation plans. Create one kebab-case directory per task (\`plan/<task-slug>/\`), keep the task overview/index in that directory's \`README.md\`, and add supporting UPPER_SNAKE_CASE plan files alongside it. Ignored by git by default.
|
|
1943
1960
|
- \`archive/\`: local completed plans, temporary reports, historical notes, payload captures. Move completed plan task directories to \`archive/plan/<task-slug>/\`; put temporary reports and investigations under \`archive/report/<report-slug>/\`. Ignored by git by default.`;
|
|
1944
1961
|
}
|
|
1945
|
-
function
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
Long-term documentation structure for this project.
|
|
1949
|
-
|
|
1950
|
-
## Top-Level Areas
|
|
1951
|
-
|
|
1952
|
-
- \`docs/spec/\`: product behavior, API contracts, user-facing requirements, and feature contracts.
|
|
1953
|
-
- \`docs/test/\`: test strategy, coverage notes, regression cases, and manual verification records.
|
|
1954
|
-
- \`docs/arch/\`: architecture decisions, code conventions, module boundaries, data flow, infrastructure/runtime dependencies, integration boundaries, and migration design.
|
|
1955
|
-
- \`docs/plan/\`: local active implementation plans.
|
|
1956
|
-
- \`docs/archive/\`: local completed plans, historical notes, payload captures, and investigation notes.
|
|
1957
|
-
|
|
1958
|
-
## Naming
|
|
1959
|
-
|
|
1960
|
-
- Directories under \`docs/\` use kebab-case.
|
|
1961
|
-
- Markdown files under \`docs/\` use UPPER_SNAKE_CASE.
|
|
1962
|
-
- \`README.md\` is the only mixed-case markdown filename exception and is required for index/overview files.
|
|
1963
|
-
|
|
1964
|
-
## File Size Guideline
|
|
1965
|
-
|
|
1966
|
-
Prefer keeping individual markdown files under 200 lines and 10,000 characters. When either guideline is exceeded, split the document into focused UPPER_SNAKE_CASE files and keep \`README.md\` as the index/overview. Configured validation exceptions should stay narrow and intentional.
|
|
1967
|
-
|
|
1968
|
-
## README Indexes
|
|
1969
|
-
|
|
1970
|
-
Each docs subdirectory \`README.md\` acts as the local table of contents. It should list important files, task directories, status, and a one-line purpose for each entry.
|
|
1971
|
-
|
|
1972
|
-
When adding, renaming, splitting, moving, or archiving docs, update the nearest relevant \`README.md\` in the same change.
|
|
1973
|
-
|
|
1974
|
-
## Domain Directory Promotion
|
|
1975
|
-
|
|
1976
|
-
Start small with one focused markdown file. When one domain grows into multiple docs, promote it to \`docs/<area>/<domain>/README.md\` and place related UPPER_SNAKE_CASE markdown files in that directory.
|
|
1977
|
-
|
|
1978
|
-
## Spec Writing Contract
|
|
1979
|
-
|
|
1980
|
-
Behavior specs describe the current product contract: supported commands, API shapes, user-visible behavior, defaults, constraints, and validation outcomes.
|
|
1981
|
-
|
|
1982
|
-
Specs should not describe how behavior changed over time. Rewrite historical-change wording into direct current-state rules. Historical context, migration rationale, future extension ideas, and completed-plan notes belong in \`docs/arch/\`, \`docs/test/\`, \`docs/archive/\`, or active \`docs/plan/\` files rather than behavior specs. If compatibility behavior is still user-visible, keep it in the spec but phrase it as a current supported or unsupported rule.
|
|
1983
|
-
|
|
1984
|
-
Config/action terms such as \`remove\`, \`exclude\`, \`fallback\`, and \`replacement semantics\` are allowed when they name current behavior precisely.
|
|
1985
|
-
|
|
1986
|
-
## Traceability Blocks
|
|
1987
|
-
|
|
1988
|
-
Behavior specs may include fenced \`json dotdotgod\` traceability blocks as the final section to connect specs to source, tests, related docs, and verification commands. The dotdotgod CLI owns the schema and validation behavior.
|
|
1989
|
-
|
|
1990
|
-
## Plan and Archive Directories
|
|
1991
|
-
|
|
1992
|
-
Active task plans use \`docs/plan/<task-slug>/README.md\`. Completed or superseded plan task directories move to \`docs/archive/plan/<task-slug>/\`. Temporary investigations, reports, payload captures, and historical notes move to \`docs/archive/report/<report-slug>/\`.
|
|
1993
|
-
|
|
1994
|
-
\`docs/plan\` and \`docs/archive\` are ignored by git by default.`;
|
|
1995
|
-
}
|
|
1996
|
-
function codeConventionsContent() {
|
|
1997
|
-
return `# Code Conventions
|
|
1998
|
-
|
|
1999
|
-
Dotdot code conventions for keeping implementation simple and maintainable.
|
|
2000
|
-
|
|
2001
|
-
## Abstraction Boundaries
|
|
2002
|
-
|
|
2003
|
-
- Do not introduce unnecessary abstractions.
|
|
2004
|
-
- Do not abstract code that is not reused.
|
|
2005
|
-
- Do not abstract reused code when the reused behavior is likely to split into separate features or flows later.
|
|
2006
|
-
- Prefer local, explicit code until a stable reuse pattern appears.
|
|
2007
|
-
|
|
2008
|
-
## Source File Size
|
|
2009
|
-
|
|
2010
|
-
- Keep source files small enough to read in one focused pass by humans and coding agents.
|
|
2011
|
-
- If code grows beyond 150 lines, consider splitting or extracting focused units even when it is not reused.
|
|
2012
|
-
- Review files approaching 250 lines for focused extraction by responsibility.
|
|
2013
|
-
- Split by behavior or responsibility, not by arbitrary layers.
|
|
2014
|
-
|
|
2015
|
-
## Dotdotgod Impact Hotspots
|
|
2016
|
-
|
|
2017
|
-
- Treat repeated \`dotdotgod graph impact\` results that collapse onto one large file as a design signal, not as normal precision.
|
|
2018
|
-
- Dotdotgod impact reveals mixed-responsibility hotspots; it does not replace focused module boundaries.
|
|
2019
|
-
- When unrelated changes keep pointing to the same source file, split the file by behavior so impact results, tests, and docs can map to narrower responsibilities.
|
|
2020
|
-
|
|
2021
|
-
## Extraction and Testability
|
|
2022
|
-
|
|
2023
|
-
- Prefer extracting pure helpers when behavior can be tested without runtime dependencies.
|
|
2024
|
-
- Keep runtime integration explicit and local until reuse is stable.
|
|
2025
|
-
- Put testable logic in focused modules before adding broad framework abstractions.
|
|
2026
|
-
- Preserve plain-text readability: avoid dense clever code, hidden control flow, and large mixed-responsibility files.`;
|
|
2027
|
-
}
|
|
2028
|
-
function initFiles(options) {
|
|
2029
|
-
const archReadmeExtra = options.dotdotSetting ? "\n\n## Index\n\n- `DOCS_STRUCTURE.md`: documentation layout, naming, README index, spec current-state writing contract, and domain directory promotion rules.\n- `CODE_CONVENTIONS.md`: dotdot code conventions, including abstraction boundaries, source file size guidance, impact hotspot handling, and extraction/testability rules. If conventions grow across multiple topics, promote them to `conventions/README.md` with supporting UPPER_SNAKE_CASE files." : "";
|
|
1962
|
+
function initFiles(options, scaffold) {
|
|
1963
|
+
const domainMap = scaffold.filter((item) => item.type === "file").map((item) => `- \`${item.path.replace(/README\.md$/, "")}\`: ${item.content.split("\n\n")[1]}`).join("\n");
|
|
2030
1964
|
const files = [
|
|
2031
|
-
["AGENTS.md", agentContent(options.projectName,
|
|
1965
|
+
["AGENTS.md", agentContent(options.projectName, domainMap)],
|
|
2032
1966
|
["CLAUDE.md", "# CLAUDE.md\n\n@AGENTS.md"],
|
|
2033
1967
|
["CODEX.md", "# CODEX.md\n\nSee [AGENTS.md](./AGENTS.md)."],
|
|
2034
|
-
["docs/README.md", docsReadmeContent()],
|
|
2035
|
-
["docs/spec/README.md", "# Specs\n\nUse this area for behavior specs, API contracts, and product requirements.\n\nFor projects using the dotdotgod CLI, behavior specs may be required by `dotdotgod validate` to include fenced `json dotdotgod` traceability blocks as the final section. The CLI owns the schema and prints property-level repair guidance when validation fails."],
|
|
2036
|
-
["docs/test/README.md", "# Tests\n\nUse this area for test strategy, coverage notes, regression cases, and manual verification records."],
|
|
2037
|
-
["docs/arch/README.md", `# Architecture
|
|
2038
|
-
|
|
2039
|
-
Use this area for architecture decisions, code conventions, module boundaries, data flow notes, infrastructure/runtime dependencies, integration boundaries, and migration design.${archReadmeExtra}`],
|
|
1968
|
+
["docs/README.md", docsReadmeContent(domainMap)],
|
|
2040
1969
|
["docs/plan/README.md", "# Plans\n\nUse this area for active implementation plans.\n\n## Naming\n\n- Task directories use kebab-case: `docs/plan/<task-slug>/`.\n- Markdown file names use UPPER_SNAKE_CASE: `README.md`, `RESEARCH_NOTES.md`, `VERIFICATION.md`.\n\n## Structure\n\n- Create one directory per task: `docs/plan/<task-slug>/`.\n- Put the task overview, index, scope, status, and main plan in `docs/plan/<task-slug>/README.md`.\n- Add supporting research, checklists, payload captures, or verification notes as additional UPPER_SNAKE_CASE markdown files in the same task directory.\n- Move completed or superseded task directories to `docs/archive/plan/<task-slug>/`.\n\nThis directory is local-only and ignored by git by default."],
|
|
2041
1970
|
["docs/archive/README.md", "# Archive\n\nUse this area for local completed plans, temporary reports, historical notes, payload captures, and investigation notes.\n\n## Naming\n\n- Archived plan task directories preserve their kebab-case task slug.\n- Archived report directories use a focused kebab-case report slug.\n- Markdown file names use UPPER_SNAKE_CASE, including `README.md`.\n\n## Structure\n\n- Move completed plan task directories from `docs/plan/<task-slug>/` to `docs/archive/plan/<task-slug>/`.\n- Put temporary investigations, reports, payload captures, and historical notes under `docs/archive/report/<report-slug>/`.\n- Preserve each archive directory's `README.md` overview/index and supporting UPPER_SNAKE_CASE markdown files.\n- Additional archive categories can be added later as focused kebab-case subdirectories when needed.\n\nThis directory is local-only and ignored by git by default."]
|
|
2042
1971
|
];
|
|
2043
|
-
if (options.dotdotSetting) {
|
|
2044
|
-
files.push(["docs/arch/DOCS_STRUCTURE.md", docsStructureContent()]);
|
|
2045
|
-
files.push(["docs/arch/CODE_CONVENTIONS.md", codeConventionsContent()]);
|
|
2046
|
-
}
|
|
2047
1972
|
return files.map(([path, content]) => [path.replace(/^docs(?=\/|$)/, options.documentationRoot), content.replaceAll("docs/", `${options.documentationRoot}/`).replaceAll("`docs`", `\`${options.documentationRoot}\``)]);
|
|
2048
1973
|
}
|
|
2049
1974
|
function runInit(argv, usage2) {
|
|
@@ -2087,7 +2012,8 @@ function runInit(argv, usage2) {
|
|
|
2087
2012
|
` };
|
|
2088
2013
|
}
|
|
2089
2014
|
const actions = [];
|
|
2090
|
-
|
|
2015
|
+
const scaffold = template?.source === "bundled" ? builtInTemplateScaffold(template.name) : [];
|
|
2016
|
+
for (const [relativePath, content] of initFiles(options, scaffold)) writeInitFile(options, relativePath, content, actions);
|
|
2091
2017
|
if (template?.source === "bundled") {
|
|
2092
2018
|
for (const item of builtInTemplateScaffold(template.name, { documentationRoot: options.documentationRoot }) ?? []) {
|
|
2093
2019
|
if (item.type === "directory") ensureInitDirectory(options, item.path, actions);
|
|
@@ -2103,7 +2029,7 @@ function runInit(argv, usage2) {
|
|
|
2103
2029
|
initError(options, "CLAUDE_SETTINGS_CHANGED", error.message);
|
|
2104
2030
|
}
|
|
2105
2031
|
}
|
|
2106
|
-
const payload = { ok: true, command: "init", root: options.root, projectName: options.projectName, documentationRoot: options.documentationRoot, dryRun: options.dryRun,
|
|
2032
|
+
const payload = { ok: true, command: "init", root: options.root, projectName: options.projectName, documentationRoot: options.documentationRoot, dryRun: options.dryRun, template: template ? { name: template.name, source: template.source, selectedBy: template.selectedBy } : null, actions };
|
|
2107
2033
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
2108
2034
|
else {
|
|
2109
2035
|
if (template) console.log(`template ${template.source}:${template.name} (${template.selectedBy})`);
|
|
@@ -2123,7 +2049,7 @@ function commandUsage(command = "root") {
|
|
|
2123
2049
|
dotdotgod validate <root> [--include-local-memory] [--check-index] [--max-lines n] [--max-chars n] [--no-link-check] [--json]`;
|
|
2124
2050
|
case "init":
|
|
2125
2051
|
return `Usage:
|
|
2126
|
-
dotdotgod init <root> [--project-name NAME] [--template NAME] [--documentation-root PATH] [--
|
|
2052
|
+
dotdotgod init <root> [--project-name NAME] [--template NAME] [--documentation-root PATH] [--claude-plans] [--dry-run] [--json]
|
|
2127
2053
|
|
|
2128
2054
|
Create AGENTS.md, agent entrypoints, docs indexes, a project config from an initialization template, and local memory gitignore entries.
|
|
2129
2055
|
Use --claude-plans only after opting into project-local native plan storage; existing Claude settings are preserved.`;
|
|
@@ -2185,7 +2111,8 @@ Checks or writes generated Markdown traceability link sections from canonical fe
|
|
|
2185
2111
|
case "graph":
|
|
2186
2112
|
return `Usage:
|
|
2187
2113
|
dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
|
|
2188
|
-
dotdotgod graph serve <root>
|
|
2114
|
+
dotdotgod graph serve <root> [--host HOST] [--port PORT]
|
|
2115
|
+
dotdotgod graph export <root> [--json]
|
|
2189
2116
|
dotdotgod graph communities <root> [--json]`;
|
|
2190
2117
|
case "graph impact":
|
|
2191
2118
|
return `Usage:
|
|
@@ -2194,9 +2121,14 @@ Checks or writes generated Markdown traceability link sections from canonical fe
|
|
|
2194
2121
|
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.`;
|
|
2195
2122
|
case "graph serve":
|
|
2196
2123
|
return `Usage:
|
|
2197
|
-
dotdotgod graph serve <root>
|
|
2124
|
+
dotdotgod graph serve <root> [--host HOST] [--port PORT]
|
|
2198
2125
|
|
|
2199
|
-
Starts a local
|
|
2126
|
+
Starts a standalone local traceability graph without Obsidian.`;
|
|
2127
|
+
case "graph export":
|
|
2128
|
+
return `Usage:
|
|
2129
|
+
dotdotgod graph export <root> [--json]
|
|
2130
|
+
|
|
2131
|
+
Write .dotdotgod/traceability.json for the Obsidian plugin.`;
|
|
2200
2132
|
case "graph communities":
|
|
2201
2133
|
return `Usage:
|
|
2202
2134
|
dotdotgod graph communities <root> [--json]`;
|
|
@@ -2220,7 +2152,8 @@ Commands:
|
|
|
2220
2152
|
status Show local index status.
|
|
2221
2153
|
traceability links Check or write generated traceability links.
|
|
2222
2154
|
graph impact Find files related to changed files.
|
|
2223
|
-
graph serve Open a local
|
|
2155
|
+
graph serve Open a standalone local traceability graph.
|
|
2156
|
+
graph export Export traceability data for Obsidian.
|
|
2224
2157
|
graph communities Find groups of related project-memory nodes.
|
|
2225
2158
|
|
|
2226
2159
|
Options:
|
|
@@ -2731,19 +2664,19 @@ function addConfiguredLocalMemoryCandidates(root, candidates, config, limit = 5e
|
|
|
2731
2664
|
let added = 0;
|
|
2732
2665
|
const addPath = (path) => {
|
|
2733
2666
|
if (added >= limit || !existsSync7(path)) return;
|
|
2734
|
-
let
|
|
2667
|
+
let stat2;
|
|
2735
2668
|
try {
|
|
2736
|
-
|
|
2669
|
+
stat2 = statSync2(path);
|
|
2737
2670
|
} catch {
|
|
2738
2671
|
return;
|
|
2739
2672
|
}
|
|
2740
2673
|
const pathRel = rel(root, path);
|
|
2741
|
-
if (
|
|
2674
|
+
if (stat2.isFile()) {
|
|
2742
2675
|
candidates.add(pathRel);
|
|
2743
2676
|
added += 1;
|
|
2744
2677
|
return;
|
|
2745
2678
|
}
|
|
2746
|
-
if (!
|
|
2679
|
+
if (!stat2.isDirectory() || isExcludedIndexDir(pathRel)) return;
|
|
2747
2680
|
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
2748
2681
|
if (added >= limit) return;
|
|
2749
2682
|
addPath(join6(path, entry.name));
|
|
@@ -3226,13 +3159,13 @@ var OUTLINE_SCHEMA = 1;
|
|
|
3226
3159
|
var outlinePath = (root) => join11(root, ".dotdotgod", "outlines.json");
|
|
3227
3160
|
function assertCachePath(root) {
|
|
3228
3161
|
for (const path of [dirname8(outlinePath(root)), outlinePath(root)]) {
|
|
3229
|
-
let
|
|
3162
|
+
let stat2;
|
|
3230
3163
|
try {
|
|
3231
|
-
|
|
3164
|
+
stat2 = lstatSync3(path);
|
|
3232
3165
|
} catch (error) {
|
|
3233
3166
|
if (error.code !== "ENOENT") throw error;
|
|
3234
3167
|
}
|
|
3235
|
-
if (
|
|
3168
|
+
if (stat2?.isSymbolicLink()) throw new Error(`Outline cache must not use symlinks: ${path}`);
|
|
3236
3169
|
}
|
|
3237
3170
|
}
|
|
3238
3171
|
function readOutlines(root) {
|
|
@@ -3937,12 +3870,196 @@ function runConfig(argv) {
|
|
|
3937
3870
|
// packages/cli/src/commands/query.mjs
|
|
3938
3871
|
import { resolve as resolve11 } from "node:path";
|
|
3939
3872
|
|
|
3873
|
+
// packages/cli/src/query/cache-session.mjs
|
|
3874
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3875
|
+
import { mkdirSync as mkdirSync6, openSync, closeSync, readFileSync as readFileSync16, writeFileSync as writeFileSync7, renameSync as renameSync3, rmSync as rmSync2 } from "node:fs";
|
|
3876
|
+
import { join as join16 } from "node:path";
|
|
3877
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
3878
|
+
|
|
3879
|
+
// packages/cli/src/query/store.mjs
|
|
3880
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
3881
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync15, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3882
|
+
import { join as join15 } from "node:path";
|
|
3883
|
+
var VECTOR_SCHEMA_VERSION = 2;
|
|
3884
|
+
var VECTOR_DIMENSIONS = 384;
|
|
3885
|
+
function profileFingerprint(identity2) {
|
|
3886
|
+
return createHash6("sha256").update(JSON.stringify(identity2)).digest("hex");
|
|
3887
|
+
}
|
|
3888
|
+
function isValidNormalizedVectorValues(values, dimensions = VECTOR_DIMENSIONS) {
|
|
3889
|
+
if (!Number.isInteger(dimensions) || dimensions < 1 || !values || values.length % dimensions !== 0) return false;
|
|
3890
|
+
for (let offset = 0; offset < values.length; offset += dimensions) {
|
|
3891
|
+
let squaredNorm = 0;
|
|
3892
|
+
for (let index = 0; index < dimensions; index += 1) {
|
|
3893
|
+
const value = values[offset + index];
|
|
3894
|
+
if (!Number.isFinite(value) || value < -1 || value > 1) return false;
|
|
3895
|
+
squaredNorm += value * value;
|
|
3896
|
+
}
|
|
3897
|
+
if (Math.abs(Math.sqrt(squaredNorm) - 1) > 0.01) return false;
|
|
3898
|
+
}
|
|
3899
|
+
return true;
|
|
3900
|
+
}
|
|
3901
|
+
function vectorCachePaths(root) {
|
|
3902
|
+
const directory = join15(root, ".dotdotgod", "vectors");
|
|
3903
|
+
return { directory, manifest: join15(directory, "manifest.json"), chunks: join15(directory, "chunks.jsonl"), embeddings: join15(directory, "embeddings.f32") };
|
|
3904
|
+
}
|
|
3905
|
+
function readVectorCache(root, identity2 = null, diagnostics = {}) {
|
|
3906
|
+
diagnostics.reason = "missing";
|
|
3907
|
+
const paths = vectorCachePaths(root);
|
|
3908
|
+
if (!existsSync16(paths.manifest) || !existsSync16(paths.chunks) || !existsSync16(paths.embeddings)) return null;
|
|
3909
|
+
try {
|
|
3910
|
+
const manifest = JSON.parse(readFileSync15(paths.manifest, "utf8"));
|
|
3911
|
+
diagnostics.reason = "schema-mismatch";
|
|
3912
|
+
if (manifest.schemaVersion !== VECTOR_SCHEMA_VERSION || !Number.isInteger(manifest.dimensions) || manifest.dimensions < 1) return null;
|
|
3913
|
+
diagnostics.reason = "profile-mismatch";
|
|
3914
|
+
if (identity2 && manifest.profileFingerprint !== profileFingerprint(identity2)) return null;
|
|
3915
|
+
diagnostics.reason = "corrupt";
|
|
3916
|
+
const chunks = readFileSync15(paths.chunks, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
3917
|
+
const buffer = readFileSync15(paths.embeddings);
|
|
3918
|
+
if (buffer.byteLength !== chunks.length * manifest.dimensions * 4) return null;
|
|
3919
|
+
const vectors = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
|
|
3920
|
+
if (!isValidNormalizedVectorValues(vectors, manifest.dimensions)) return null;
|
|
3921
|
+
diagnostics.reason = "valid";
|
|
3922
|
+
return { manifest, chunks, vectors: new Float32Array(vectors) };
|
|
3923
|
+
} catch {
|
|
3924
|
+
diagnostics.reason = "corrupt";
|
|
3925
|
+
return null;
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
function atomicWrite(path, data) {
|
|
3929
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
3930
|
+
writeFileSync6(temporary, data);
|
|
3931
|
+
renameSync2(temporary, path);
|
|
3932
|
+
}
|
|
3933
|
+
function writeVectorCache(root, chunks, vectors, metadata = {}) {
|
|
3934
|
+
const paths = vectorCachePaths(root);
|
|
3935
|
+
const dimensions = metadata.dimensions;
|
|
3936
|
+
if (!Number.isInteger(dimensions) || dimensions < 1) throw new Error("Vector dimensions must be a positive integer.");
|
|
3937
|
+
mkdirSync5(paths.directory, { recursive: true });
|
|
3938
|
+
if (vectors.length !== chunks.length * dimensions) throw new Error("Vector count does not match chunk metadata.");
|
|
3939
|
+
if (!isValidNormalizedVectorValues(vectors, dimensions)) throw new Error("Vectors must contain finite normalized values.");
|
|
3940
|
+
const manifest = { schemaVersion: VECTOR_SCHEMA_VERSION, chunks: chunks.length, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...metadata };
|
|
3941
|
+
atomicWrite(paths.chunks, `${chunks.map((chunk) => JSON.stringify(chunk)).join("\n")}
|
|
3942
|
+
`);
|
|
3943
|
+
atomicWrite(paths.embeddings, Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength));
|
|
3944
|
+
atomicWrite(paths.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
3945
|
+
`);
|
|
3946
|
+
return manifest;
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
// packages/cli/src/query/cache-session.mjs
|
|
3950
|
+
async function vectorCacheSession(root, identity2, action2) {
|
|
3951
|
+
const directory = join16(root, ".dotdotgod", "vectors");
|
|
3952
|
+
mkdirSync6(directory, { recursive: true });
|
|
3953
|
+
const lock = join16(directory, "build.lock");
|
|
3954
|
+
const token = randomUUID2();
|
|
3955
|
+
const started = Date.now();
|
|
3956
|
+
let waited = false;
|
|
3957
|
+
while (true) {
|
|
3958
|
+
try {
|
|
3959
|
+
const fd = openSync(lock, "wx");
|
|
3960
|
+
try {
|
|
3961
|
+
writeFileSync7(fd, JSON.stringify({ pid: process.pid, token }));
|
|
3962
|
+
} finally {
|
|
3963
|
+
closeSync(fd);
|
|
3964
|
+
}
|
|
3965
|
+
break;
|
|
3966
|
+
} catch (error) {
|
|
3967
|
+
if (error.code !== "EEXIST") throw error;
|
|
3968
|
+
waited = true;
|
|
3969
|
+
if (Date.now() - started >= 3e4) throw new Error("Vector cache lock timed out; another build may be running or the lock needs inspection.");
|
|
3970
|
+
try {
|
|
3971
|
+
const owner = JSON.parse(readFileSync16(lock, "utf8"));
|
|
3972
|
+
if (!Number.isInteger(owner.pid) || owner.pid < 1) throw new Error("Invalid lock owner.");
|
|
3973
|
+
try {
|
|
3974
|
+
process.kill(owner.pid, 0);
|
|
3975
|
+
} catch (probe) {
|
|
3976
|
+
if (probe.code !== "ESRCH") throw probe;
|
|
3977
|
+
const recovery = `${lock}.recovery`;
|
|
3978
|
+
try {
|
|
3979
|
+
mkdirSync6(recovery);
|
|
3980
|
+
} catch (claim) {
|
|
3981
|
+
if (claim.code === "EEXIST") {
|
|
3982
|
+
await sleep(50);
|
|
3983
|
+
continue;
|
|
3984
|
+
}
|
|
3985
|
+
throw claim;
|
|
3986
|
+
}
|
|
3987
|
+
try {
|
|
3988
|
+
const current = JSON.parse(readFileSync16(lock, "utf8"));
|
|
3989
|
+
if (current.token === owner.token && current.pid === owner.pid) rmSync2(lock);
|
|
3990
|
+
} finally {
|
|
3991
|
+
rmSync2(recovery, { recursive: true, force: true });
|
|
3992
|
+
}
|
|
3993
|
+
continue;
|
|
3994
|
+
}
|
|
3995
|
+
} catch (probe) {
|
|
3996
|
+
if (probe.code === "ENOENT") continue;
|
|
3997
|
+
}
|
|
3998
|
+
await sleep(50);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
try {
|
|
4002
|
+
const path = join16(directory, "snapshots.json");
|
|
4003
|
+
const fingerprint2 = profileFingerprint(identity2);
|
|
4004
|
+
let state;
|
|
4005
|
+
let reason = "missing";
|
|
4006
|
+
try {
|
|
4007
|
+
state = JSON.parse(readFileSync16(path, "utf8"));
|
|
4008
|
+
if (state.schema !== 1) {
|
|
4009
|
+
reason = "schema-mismatch";
|
|
4010
|
+
state = null;
|
|
4011
|
+
} else if (state.profile !== fingerprint2) {
|
|
4012
|
+
reason = "profile-mismatch";
|
|
4013
|
+
state = null;
|
|
4014
|
+
} else {
|
|
4015
|
+
if (state.dimensions !== null && (!Number.isInteger(state.dimensions) || state.dimensions < 1)) throw new Error("Invalid cache dimensions.");
|
|
4016
|
+
for (const rows of [state.docs, state.code, state.pending]) {
|
|
4017
|
+
if (!Array.isArray(rows) || rows.some((row) => typeof row.chunk?.fingerprint !== "string" || !Array.isArray(row.vector) || row.vector.length !== state.dimensions || !isValidNormalizedVectorValues(row.vector, state.dimensions))) throw new Error("Invalid cache rows.");
|
|
4018
|
+
}
|
|
4019
|
+
reason = "valid";
|
|
4020
|
+
}
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
state = null;
|
|
4023
|
+
reason = error.code === "ENOENT" ? "missing" : "corrupt";
|
|
4024
|
+
}
|
|
4025
|
+
if (!state) {
|
|
4026
|
+
state = { schema: 1, profile: fingerprint2, dimensions: null, docs: [], code: [], pending: [] };
|
|
4027
|
+
if (reason === "missing") {
|
|
4028
|
+
const legacyDiagnostics = {};
|
|
4029
|
+
const legacy = readVectorCache(root, identity2, legacyDiagnostics);
|
|
4030
|
+
reason = legacyDiagnostics.reason;
|
|
4031
|
+
if (legacy) {
|
|
4032
|
+
state.dimensions = legacy.manifest.dimensions;
|
|
4033
|
+
legacy.chunks.forEach((chunk, i) => state[chunk.kind === "symbol" ? "code" : "docs"].push({ chunk, vector: Array.from(legacy.vectors.subarray(i * state.dimensions, (i + 1) * state.dimensions)) }));
|
|
4034
|
+
reason = "legacy-migrated";
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
const save = () => {
|
|
4039
|
+
const temporary = `${path}.tmp-${token}`;
|
|
4040
|
+
try {
|
|
4041
|
+
writeFileSync7(temporary, JSON.stringify(state));
|
|
4042
|
+
renameSync3(temporary, path);
|
|
4043
|
+
} finally {
|
|
4044
|
+
rmSync2(temporary, { force: true });
|
|
4045
|
+
}
|
|
4046
|
+
};
|
|
4047
|
+
return await action2(state, save, { reason, lockWaitMs: waited ? Date.now() - started : 0 });
|
|
4048
|
+
} finally {
|
|
4049
|
+
try {
|
|
4050
|
+
if (JSON.parse(readFileSync16(lock, "utf8")).token === token) rmSync2(lock);
|
|
4051
|
+
} catch (error) {
|
|
4052
|
+
if (error.code !== "ENOENT") throw error;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
|
|
3940
4057
|
// packages/cli/src/query/embedding-runtime.mjs
|
|
3941
|
-
import { existsSync as
|
|
4058
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync17, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
3942
4059
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3943
4060
|
import { createRequire as createRequire2 } from "node:module";
|
|
3944
4061
|
import { homedir as homedir3 } from "node:os";
|
|
3945
|
-
import { join as
|
|
4062
|
+
import { join as join17 } from "node:path";
|
|
3946
4063
|
import { pathToFileURL } from "node:url";
|
|
3947
4064
|
var EMBEDDING_RUNTIME_PACKAGE = "@huggingface/transformers";
|
|
3948
4065
|
var EMBEDDING_RUNTIME_VERSION = "4.2.0";
|
|
@@ -3954,12 +4071,12 @@ var EmbeddingRuntimeMissingError = class extends Error {
|
|
|
3954
4071
|
}
|
|
3955
4072
|
};
|
|
3956
4073
|
function embeddingRuntimeRoot(options = {}) {
|
|
3957
|
-
return
|
|
4074
|
+
return join17(options.home ?? homedir3(), ".dotdotgod", "runtime", "embedding");
|
|
3958
4075
|
}
|
|
3959
4076
|
function resolvePersistentTransformers(options = {}) {
|
|
3960
4077
|
const root = embeddingRuntimeRoot(options);
|
|
3961
4078
|
try {
|
|
3962
|
-
const require3 = createRequire2(
|
|
4079
|
+
const require3 = createRequire2(join17(root, "package.json"));
|
|
3963
4080
|
return require3.resolve(EMBEDDING_RUNTIME_PACKAGE);
|
|
3964
4081
|
} catch {
|
|
3965
4082
|
throw new EmbeddingRuntimeMissingError();
|
|
@@ -3968,10 +4085,10 @@ function resolvePersistentTransformers(options = {}) {
|
|
|
3968
4085
|
function embeddingRuntimeStatus(options = {}) {
|
|
3969
4086
|
const root = embeddingRuntimeRoot(options);
|
|
3970
4087
|
try {
|
|
3971
|
-
const require3 = createRequire2(
|
|
4088
|
+
const require3 = createRequire2(join17(root, "package.json"));
|
|
3972
4089
|
require3.resolve(EMBEDDING_RUNTIME_PACKAGE);
|
|
3973
|
-
const packagePath =
|
|
3974
|
-
const version = JSON.parse(
|
|
4090
|
+
const packagePath = join17(root, "node_modules", ...EMBEDDING_RUNTIME_PACKAGE.split("/"), "package.json");
|
|
4091
|
+
const version = JSON.parse(readFileSync17(packagePath, "utf8")).version;
|
|
3975
4092
|
return { ok: true, installed: true, installAvailable: true, package: EMBEDDING_RUNTIME_PACKAGE, requiredVersion: EMBEDDING_RUNTIME_VERSION, packageVersion: version, location: root };
|
|
3976
4093
|
} catch {
|
|
3977
4094
|
return { ok: true, installed: false, installAvailable: true, package: EMBEDDING_RUNTIME_PACKAGE, requiredVersion: EMBEDDING_RUNTIME_VERSION, packageVersion: null, location: root };
|
|
@@ -3980,10 +4097,10 @@ function embeddingRuntimeStatus(options = {}) {
|
|
|
3980
4097
|
function installEmbeddingRuntime(options = {}) {
|
|
3981
4098
|
if (options.confirm !== true) throw new Error("Embedding runtime installation requires explicit confirmation.");
|
|
3982
4099
|
const root = embeddingRuntimeRoot(options);
|
|
3983
|
-
const lock =
|
|
3984
|
-
if (
|
|
3985
|
-
|
|
3986
|
-
|
|
4100
|
+
const lock = join17(root, ".installing");
|
|
4101
|
+
if (existsSync17(lock)) throw new Error("Embedding runtime installation is already in progress.");
|
|
4102
|
+
mkdirSync7(root, { recursive: true });
|
|
4103
|
+
writeFileSync8(lock, `${process.pid}
|
|
3987
4104
|
`, { flag: "wx" });
|
|
3988
4105
|
try {
|
|
3989
4106
|
const spawn = options.spawnImpl ?? spawnSync2;
|
|
@@ -3993,7 +4110,7 @@ function installEmbeddingRuntime(options = {}) {
|
|
|
3993
4110
|
if (!status.installed) throw new Error("Embedding runtime installation could not be verified.");
|
|
3994
4111
|
return { ...status, command: "embedding install", modelDownloadPending: true };
|
|
3995
4112
|
} finally {
|
|
3996
|
-
|
|
4113
|
+
rmSync3(lock, { force: true });
|
|
3997
4114
|
}
|
|
3998
4115
|
}
|
|
3999
4116
|
async function importTransformers(options = {}) {
|
|
@@ -4103,70 +4220,6 @@ async function resolveEmbedder(root, options = {}) {
|
|
|
4103
4220
|
return { ...resolved, identity: embeddingProfileIdentity(resolved.profile), embed: options.embed ? withInputFormat(options.embed, resolved.profile) : await createEmbedder(resolved.profile) };
|
|
4104
4221
|
}
|
|
4105
4222
|
|
|
4106
|
-
// packages/cli/src/query/store.mjs
|
|
4107
|
-
import { createHash as createHash6 } from "node:crypto";
|
|
4108
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync6, readFileSync as readFileSync16, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4109
|
-
import { join as join16 } from "node:path";
|
|
4110
|
-
var VECTOR_SCHEMA_VERSION = 2;
|
|
4111
|
-
var VECTOR_DIMENSIONS = 384;
|
|
4112
|
-
function profileFingerprint(identity2) {
|
|
4113
|
-
return createHash6("sha256").update(JSON.stringify(identity2)).digest("hex");
|
|
4114
|
-
}
|
|
4115
|
-
function isValidNormalizedVectorValues(values, dimensions = VECTOR_DIMENSIONS) {
|
|
4116
|
-
if (!Number.isInteger(dimensions) || dimensions < 1 || !values || values.length % dimensions !== 0) return false;
|
|
4117
|
-
for (let offset = 0; offset < values.length; offset += dimensions) {
|
|
4118
|
-
let squaredNorm = 0;
|
|
4119
|
-
for (let index = 0; index < dimensions; index += 1) {
|
|
4120
|
-
const value = values[offset + index];
|
|
4121
|
-
if (!Number.isFinite(value) || value < -1 || value > 1) return false;
|
|
4122
|
-
squaredNorm += value * value;
|
|
4123
|
-
}
|
|
4124
|
-
if (Math.abs(Math.sqrt(squaredNorm) - 1) > 0.01) return false;
|
|
4125
|
-
}
|
|
4126
|
-
return true;
|
|
4127
|
-
}
|
|
4128
|
-
function vectorCachePaths(root) {
|
|
4129
|
-
const directory = join16(root, ".dotdotgod", "vectors");
|
|
4130
|
-
return { directory, manifest: join16(directory, "manifest.json"), chunks: join16(directory, "chunks.jsonl"), embeddings: join16(directory, "embeddings.f32") };
|
|
4131
|
-
}
|
|
4132
|
-
function readVectorCache(root, identity2 = null) {
|
|
4133
|
-
const paths = vectorCachePaths(root);
|
|
4134
|
-
if (!existsSync17(paths.manifest) || !existsSync17(paths.chunks) || !existsSync17(paths.embeddings)) return null;
|
|
4135
|
-
try {
|
|
4136
|
-
const manifest = JSON.parse(readFileSync16(paths.manifest, "utf8"));
|
|
4137
|
-
if (manifest.schemaVersion !== VECTOR_SCHEMA_VERSION || !Number.isInteger(manifest.dimensions) || manifest.dimensions < 1) return null;
|
|
4138
|
-
if (identity2 && manifest.profileFingerprint !== profileFingerprint(identity2)) return null;
|
|
4139
|
-
const chunks = readFileSync16(paths.chunks, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4140
|
-
const buffer = readFileSync16(paths.embeddings);
|
|
4141
|
-
if (buffer.byteLength !== chunks.length * manifest.dimensions * 4) return null;
|
|
4142
|
-
const vectors = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
|
|
4143
|
-
if (!isValidNormalizedVectorValues(vectors, manifest.dimensions)) return null;
|
|
4144
|
-
return { manifest, chunks, vectors: new Float32Array(vectors) };
|
|
4145
|
-
} catch {
|
|
4146
|
-
return null;
|
|
4147
|
-
}
|
|
4148
|
-
}
|
|
4149
|
-
function atomicWrite(path, data) {
|
|
4150
|
-
const temporary = `${path}.tmp-${process.pid}`;
|
|
4151
|
-
writeFileSync7(temporary, data);
|
|
4152
|
-
renameSync2(temporary, path);
|
|
4153
|
-
}
|
|
4154
|
-
function writeVectorCache(root, chunks, vectors, metadata = {}) {
|
|
4155
|
-
const paths = vectorCachePaths(root);
|
|
4156
|
-
const dimensions = metadata.dimensions;
|
|
4157
|
-
if (!Number.isInteger(dimensions) || dimensions < 1) throw new Error("Vector dimensions must be a positive integer.");
|
|
4158
|
-
mkdirSync6(paths.directory, { recursive: true });
|
|
4159
|
-
if (vectors.length !== chunks.length * dimensions) throw new Error("Vector count does not match chunk metadata.");
|
|
4160
|
-
if (!isValidNormalizedVectorValues(vectors, dimensions)) throw new Error("Vectors must contain finite normalized values.");
|
|
4161
|
-
const manifest = { schemaVersion: VECTOR_SCHEMA_VERSION, chunks: chunks.length, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...metadata };
|
|
4162
|
-
atomicWrite(paths.chunks, `${chunks.map((chunk) => JSON.stringify(chunk)).join("\n")}
|
|
4163
|
-
`);
|
|
4164
|
-
atomicWrite(paths.embeddings, Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength));
|
|
4165
|
-
atomicWrite(paths.manifest, `${JSON.stringify(manifest, null, 2)}
|
|
4166
|
-
`);
|
|
4167
|
-
return manifest;
|
|
4168
|
-
}
|
|
4169
|
-
|
|
4170
4223
|
// packages/cli/src/query/hybrid.mjs
|
|
4171
4224
|
var resultKey = (chunk) => chunk.kind === "symbol" ? chunk.id : chunk.path;
|
|
4172
4225
|
var terms = (text) => [...new Set(text.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean))];
|
|
@@ -4284,38 +4337,62 @@ async function buildVectorIndex(root, embedOrOptions = {}) {
|
|
|
4284
4337
|
const config = readMemoryConfig(root);
|
|
4285
4338
|
const exclude = config.load?.documentationSummary?.exclude ?? ["docs/plan", "docs/archive"];
|
|
4286
4339
|
const chunks = options.chunks ?? collectDocumentationChunks(root, exclude, config.documentation?.root ?? "docs");
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
const
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
rows[batch[index].index] = vector;
|
|
4340
|
+
return vectorCacheSession(root, resolved.identity, async (state, save, diagnostics) => {
|
|
4341
|
+
const selectedFingerprints = new Set(chunks.map((chunk) => chunk.fingerprint));
|
|
4342
|
+
state.pending = state.pending.filter((row) => selectedFingerprints.has(row.chunk.fingerprint));
|
|
4343
|
+
const recovered = new Set(state.pending.map((row) => row.chunk.fingerprint));
|
|
4344
|
+
const cacheRows = [...state.docs, ...state.code, ...state.pending];
|
|
4345
|
+
const cached = state.dimensions ? { chunks: cacheRows.map((row) => row.chunk), vectors: Float32Array.from(cacheRows.flatMap((row) => row.vector)), manifest: { dimensions: state.dimensions } } : null;
|
|
4346
|
+
const cachedOffsets = new Map((cached?.chunks ?? []).map((chunk, index) => [chunk.fingerprint, index]));
|
|
4347
|
+
const rows = new Array(chunks.length);
|
|
4348
|
+
const missing = [];
|
|
4349
|
+
let reused = 0;
|
|
4350
|
+
chunks.forEach((chunk, index) => {
|
|
4351
|
+
const oldIndex = cachedOffsets.get(chunk.fingerprint);
|
|
4352
|
+
if (oldIndex === void 0 || !cached) missing.push({ chunk, index });
|
|
4353
|
+
else {
|
|
4354
|
+
const d = cached.manifest.dimensions;
|
|
4355
|
+
rows[index] = Array.from(cached.vectors.subarray(oldIndex * d, (oldIndex + 1) * d));
|
|
4356
|
+
reused += 1;
|
|
4357
|
+
}
|
|
4306
4358
|
});
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4359
|
+
for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
|
|
4360
|
+
const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
|
|
4361
|
+
const embedded = await resolved.embed(batch.map(({ chunk }) => chunk.passage), "document");
|
|
4362
|
+
if (!Array.isArray(embedded) || embedded.length !== batch.length) throw new Error("Embedder returned an unexpected vector count.");
|
|
4363
|
+
embedded.forEach((vector, index) => {
|
|
4364
|
+
const dimensions2 = state.dimensions ?? vector.length;
|
|
4365
|
+
assertVector(vector, dimensions2, "Passage vector");
|
|
4366
|
+
state.dimensions = dimensions2;
|
|
4367
|
+
rows[batch[index].index] = vector;
|
|
4368
|
+
const { passage, ...chunk } = batch[index].chunk;
|
|
4369
|
+
state.pending.push({ chunk, vector: Array.from(vector) });
|
|
4370
|
+
});
|
|
4371
|
+
save();
|
|
4372
|
+
}
|
|
4373
|
+
const dimensions = rows[0]?.length ?? cached?.manifest?.dimensions;
|
|
4374
|
+
if (!dimensions && chunks.length) throw new Error("No documentation chunks were available to establish vector dimensions.");
|
|
4375
|
+
if (rows.some((row) => !row || row.length !== dimensions)) throw new Error("Embedder returned inconsistent passage-vector dimensions.");
|
|
4376
|
+
const vectors = new Float32Array(chunks.length * dimensions);
|
|
4377
|
+
rows.forEach((row, index) => {
|
|
4378
|
+
assertVector(row, dimensions, "Passage vector");
|
|
4379
|
+
vectors.set(row, index * dimensions);
|
|
4380
|
+
});
|
|
4381
|
+
const storedChunks = chunks.map(({ passage, ...chunk }) => chunk);
|
|
4382
|
+
const refreshed = options.corpora ?? (options.chunks ? [...new Set(chunks.map((chunk) => chunk.kind === "symbol" ? "code" : "docs"))] : ["docs"]);
|
|
4383
|
+
const counters = {};
|
|
4384
|
+
for (const corpus of refreshed) {
|
|
4385
|
+
const positions = chunks.map((chunk, index) => ({ chunk, index })).filter(({ chunk }) => (chunk.kind === "symbol" ? "code" : "docs") === corpus);
|
|
4386
|
+
state[corpus] = positions.map(({ index }) => ({ chunk: storedChunks[index], vector: Array.from(rows[index]) }));
|
|
4387
|
+
counters[corpus] = { chunks: positions.length, embedded: missing.filter(({ chunk }) => (chunk.kind === "symbol" ? "code" : "docs") === corpus).length, recovered: positions.filter(({ chunk }) => recovered.has(chunk.fingerprint)).length };
|
|
4388
|
+
counters[corpus].reused = positions.length - counters[corpus].embedded;
|
|
4389
|
+
}
|
|
4390
|
+
state.pending = [];
|
|
4391
|
+
save();
|
|
4392
|
+
const metadata = { provider: resolved.profile.provider, model: resolved.profile.model, dimensions, profileFingerprint: profileFingerprint(resolved.identity), excluded: exclude, embedded: missing.length, reused, diagnostics: { ...diagnostics, corpora: counters } };
|
|
4393
|
+
const manifest = dimensions ? writeVectorCache(root, storedChunks, vectors, metadata) : { chunks: 0, ...metadata };
|
|
4394
|
+
return { chunks: storedChunks, vectors, manifest, embedding: resolved };
|
|
4315
4395
|
});
|
|
4316
|
-
const storedChunks = chunks.map(({ passage, ...chunk }) => chunk);
|
|
4317
|
-
const manifest = writeVectorCache(root, storedChunks, vectors, { provider: resolved.profile.provider, model: resolved.profile.model, dimensions, profileFingerprint: profileFingerprint(resolved.identity), excluded: exclude, embedded: missing.length, reused });
|
|
4318
|
-
return { chunks: storedChunks, vectors, manifest, embedding: resolved };
|
|
4319
4396
|
}
|
|
4320
4397
|
async function queryProject(root, query, options = {}) {
|
|
4321
4398
|
const scope = options.scope ?? "all";
|
|
@@ -4336,10 +4413,10 @@ async function queryProject(root, query, options = {}) {
|
|
|
4336
4413
|
}
|
|
4337
4414
|
if (!configured && !vectorError && search === "vector") throw new Error("Embedding is not configured. Set embedding in project or global configuration to use vector search.");
|
|
4338
4415
|
}
|
|
4339
|
-
if (search !== "keyword" && configured
|
|
4416
|
+
if (search !== "keyword" && configured) {
|
|
4340
4417
|
try {
|
|
4341
|
-
index = await buildVectorIndex(root, { ...options, chunks: corpus.chunks });
|
|
4342
|
-
const [queryVector] = await index.embedding.embed([query], "query");
|
|
4418
|
+
index = await buildVectorIndex(root, { ...options, chunks: corpus.chunks, corpora: scope === "all" ? ["docs", "code"] : [scope] });
|
|
4419
|
+
const [queryVector] = index.chunks.length ? await index.embedding.embed([query], "query") : [];
|
|
4343
4420
|
vector = index.chunks.map((chunk, position) => ({ ...chunk, vectorScore: cosineScore(queryVector, index.vectors, position * index.manifest.dimensions, index.manifest.dimensions) })).sort((a, b) => b.vectorScore - a.vectorScore || a.id.localeCompare(b.id));
|
|
4344
4421
|
} catch (error) {
|
|
4345
4422
|
if (search === "vector") throw error;
|
|
@@ -4430,7 +4507,7 @@ import { resolve as resolve14 } from "node:path";
|
|
|
4430
4507
|
|
|
4431
4508
|
// packages/cli/src/memory/documentation-map.mjs
|
|
4432
4509
|
import { existsSync as existsSync18, readdirSync as readdirSync4, statSync as statSync7 } from "node:fs";
|
|
4433
|
-
import { join as
|
|
4510
|
+
import { join as join18, resolve as resolve13 } from "node:path";
|
|
4434
4511
|
var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", ".git", ".dotdotgod"]);
|
|
4435
4512
|
function node() {
|
|
4436
4513
|
return { directories: /* @__PURE__ */ new Map(), files: [] };
|
|
@@ -4493,7 +4570,7 @@ function discoverMarkdown(root, documentationRoot, exclude) {
|
|
|
4493
4570
|
const walk = (relativeDirectory) => {
|
|
4494
4571
|
let entries;
|
|
4495
4572
|
try {
|
|
4496
|
-
entries = readdirSync4(
|
|
4573
|
+
entries = readdirSync4(join18(root, relativeDirectory), { withFileTypes: true, encoding: "utf8" });
|
|
4497
4574
|
} catch {
|
|
4498
4575
|
return;
|
|
4499
4576
|
}
|
|
@@ -5942,7 +6019,7 @@ function compareImpactItems(seeds) {
|
|
|
5942
6019
|
}
|
|
5943
6020
|
|
|
5944
6021
|
// packages/cli/src/impact/references.mjs
|
|
5945
|
-
import { readFileSync as
|
|
6022
|
+
import { readFileSync as readFileSync18, statSync as statSync8 } from "node:fs";
|
|
5946
6023
|
import { extname as extname7, resolve as resolve15 } from "node:path";
|
|
5947
6024
|
var WARNING_LIMIT = 20;
|
|
5948
6025
|
var edgeKey = (source, target, relation) => `${source}\0${target}\0${relation}`;
|
|
@@ -5956,9 +6033,9 @@ function prepareImpactReferences(index, root, changedPaths) {
|
|
|
5956
6033
|
let result;
|
|
5957
6034
|
try {
|
|
5958
6035
|
const absolute = resolve15(root, path);
|
|
5959
|
-
const
|
|
6036
|
+
const stat2 = statSync8(absolute);
|
|
5960
6037
|
result = { status: "exists" };
|
|
5961
|
-
if (
|
|
6038
|
+
if (stat2.isFile() && extname7(absolute) === ".md") result.anchors = extractAnchors(readFileSync18(absolute, "utf8"));
|
|
5962
6039
|
} catch (error) {
|
|
5963
6040
|
result = { status: ["ENOENT", "ENOTDIR"].includes(error.code) ? "missing" : "unavailable" };
|
|
5964
6041
|
}
|
|
@@ -6020,7 +6097,7 @@ function prepareImpactReferences(index, root, changedPaths) {
|
|
|
6020
6097
|
}
|
|
6021
6098
|
|
|
6022
6099
|
// packages/cli/src/impact/vector-profile.mjs
|
|
6023
|
-
import { lstatSync as lstatSync4, readFileSync as
|
|
6100
|
+
import { lstatSync as lstatSync4, readFileSync as readFileSync19, realpathSync as realpathSync3 } from "node:fs";
|
|
6024
6101
|
import { extname as extname8, relative as relative7, resolve as resolve16, sep } from "node:path";
|
|
6025
6102
|
var MAX_VECTOR_PROFILE_FILE_BYTES = 64 * 1024;
|
|
6026
6103
|
var MAX_VECTOR_PROFILE_CHARS = 4e3;
|
|
@@ -6079,7 +6156,7 @@ function safeTextPrefix(root, path) {
|
|
|
6079
6156
|
if (canonicalPath !== path || unsafeProfilePath(canonicalPath) || !TEXT_EXTENSIONS.has(extname8(canonicalPath).toLowerCase())) return "";
|
|
6080
6157
|
const stats = lstatSync4(canonicalAbsolute);
|
|
6081
6158
|
if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_VECTOR_PROFILE_FILE_BYTES) return "";
|
|
6082
|
-
const buffer =
|
|
6159
|
+
const buffer = readFileSync19(canonicalAbsolute);
|
|
6083
6160
|
if (buffer.includes(0)) return "";
|
|
6084
6161
|
const text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
6085
6162
|
return text.slice(0, MAX_VECTOR_PROFILE_CHARS);
|
|
@@ -6530,15 +6607,15 @@ function runExpand(argv) {
|
|
|
6530
6607
|
}
|
|
6531
6608
|
|
|
6532
6609
|
// packages/cli/src/commands/traceability.mjs
|
|
6533
|
-
import { existsSync as existsSync19, readFileSync as
|
|
6534
|
-
import { join as
|
|
6610
|
+
import { existsSync as existsSync19, readFileSync as readFileSync20, readdirSync as readdirSync5, writeFileSync as writeFileSync9 } from "node:fs";
|
|
6611
|
+
import { join as join19, resolve as resolve18 } from "node:path";
|
|
6535
6612
|
function collectDocsMarkdownFiles(root) {
|
|
6536
|
-
const docs =
|
|
6613
|
+
const docs = join19(root, "docs");
|
|
6537
6614
|
const files = [];
|
|
6538
6615
|
const walk = (dir) => {
|
|
6539
6616
|
if (!existsSync19(dir)) return;
|
|
6540
6617
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
6541
|
-
const path =
|
|
6618
|
+
const path = join19(dir, entry.name);
|
|
6542
6619
|
if (entry.isDirectory()) walk(path);
|
|
6543
6620
|
else if (entry.isFile() && entry.name.endsWith(".md")) files.push(path);
|
|
6544
6621
|
}
|
|
@@ -6574,7 +6651,7 @@ function runTraceability(argv) {
|
|
|
6574
6651
|
const errors = [...config.errors ?? []];
|
|
6575
6652
|
for (const file of files) {
|
|
6576
6653
|
if (options.write && errors.length > 0) break;
|
|
6577
|
-
const content =
|
|
6654
|
+
const content = readFileSync20(file, "utf8");
|
|
6578
6655
|
for (const error of validateTraceabilityLinksRegion(content, options.root, file)) errors.push(error);
|
|
6579
6656
|
const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
|
|
6580
6657
|
if (blocks.length === 0) continue;
|
|
@@ -6583,7 +6660,7 @@ function runTraceability(argv) {
|
|
|
6583
6660
|
for (const error of result.errors ?? []) errors.push(error);
|
|
6584
6661
|
continue;
|
|
6585
6662
|
}
|
|
6586
|
-
if (options.write && result.changed)
|
|
6663
|
+
if (options.write && result.changed) writeFileSync9(file, result.content);
|
|
6587
6664
|
if (result.changed) results.push({ file: rel(options.root, file), changed: true });
|
|
6588
6665
|
}
|
|
6589
6666
|
const ok = errors.length === 0 && (!options.check || results.length === 0);
|
|
@@ -6602,126 +6679,57 @@ function runTraceability(argv) {
|
|
|
6602
6679
|
process.exit(ok ? 0 : 1);
|
|
6603
6680
|
}
|
|
6604
6681
|
|
|
6605
|
-
// packages/cli/src/graph
|
|
6606
|
-
import {
|
|
6607
|
-
import {
|
|
6608
|
-
import { dirname as dirname13, extname as extname10, join as join19 } from "node:path";
|
|
6609
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6682
|
+
// packages/cli/src/commands/graph.mjs
|
|
6683
|
+
import { mkdir, writeFile, rename, rm } from "node:fs/promises";
|
|
6684
|
+
import { join as join21 } from "node:path";
|
|
6610
6685
|
|
|
6611
|
-
// packages/cli/src/graph-view/
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
if (
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
const
|
|
6621
|
-
|
|
6622
|
-
adjacency.get(edge.source)?.add(edge.target);
|
|
6623
|
-
adjacency.get(edge.target)?.add(edge.source);
|
|
6624
|
-
}
|
|
6625
|
-
return adjacency;
|
|
6626
|
-
}
|
|
6627
|
-
function rootReachability(nodes, edges, seedIds) {
|
|
6628
|
-
const adjacency = adjacencyFor(nodes, edges);
|
|
6629
|
-
const reachable = /* @__PURE__ */ new Set();
|
|
6630
|
-
const depthByNode = /* @__PURE__ */ new Map();
|
|
6631
|
-
const nearestRootByNode = /* @__PURE__ */ new Map();
|
|
6632
|
-
const queue = [];
|
|
6633
|
-
for (const node2 of nodes) {
|
|
6634
|
-
if (!seedIds.has(node2.id)) continue;
|
|
6635
|
-
reachable.add(node2.id);
|
|
6636
|
-
depthByNode.set(node2.id, 0);
|
|
6637
|
-
nearestRootByNode.set(node2.id, node2.id);
|
|
6638
|
-
queue.push(node2.id);
|
|
6639
|
-
}
|
|
6640
|
-
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
6641
|
-
const current = queue[cursor];
|
|
6642
|
-
for (const neighbor of adjacency.get(current) ?? []) {
|
|
6643
|
-
if (reachable.has(neighbor)) continue;
|
|
6644
|
-
reachable.add(neighbor);
|
|
6645
|
-
depthByNode.set(neighbor, (depthByNode.get(current) ?? 0) + 1);
|
|
6646
|
-
nearestRootByNode.set(neighbor, nearestRootByNode.get(current));
|
|
6647
|
-
queue.push(neighbor);
|
|
6648
|
-
}
|
|
6649
|
-
}
|
|
6650
|
-
return { reachable, depthByNode, nearestRootByNode };
|
|
6651
|
-
}
|
|
6652
|
-
function buildImpactGraphPayload(index, impact) {
|
|
6653
|
-
const graph = impact.structuralGraph ?? index?.graph ?? { nodes: [], edges: [] };
|
|
6654
|
-
const config = index?.memoryConfig;
|
|
6655
|
-
const seedIds = new Set((impact.changedFiles ?? [impact.changed]).map((path) => `file:${path}`));
|
|
6656
|
-
const impactItems = new Map((impact.related ?? []).map((item) => [item.id, item]));
|
|
6657
|
-
const isAllowedNode = (node2) => {
|
|
6658
|
-
if (!node2) return false;
|
|
6659
|
-
if (node2.path && resolveMemoryArea(node2.path, config)?.scope === "local") return false;
|
|
6660
|
-
return true;
|
|
6661
|
-
};
|
|
6662
|
-
const sourceNodes = (graph.nodes ?? []).filter(isAllowedNode);
|
|
6663
|
-
const indexedNodes = new Map(sourceNodes.map((node2) => [node2.id, node2]));
|
|
6664
|
-
const seenEdges = /* @__PURE__ */ new Set();
|
|
6686
|
+
// packages/cli/src/graph-view/traceability.mjs
|
|
6687
|
+
import { resolve as resolve19 } from "node:path";
|
|
6688
|
+
import { stat } from "node:fs/promises";
|
|
6689
|
+
async function exportTraceability(root) {
|
|
6690
|
+
const config = readMemoryConfig(root);
|
|
6691
|
+
if (config.errors?.length) throw new Error(JSON.stringify(config.errors));
|
|
6692
|
+
const { index } = readFreshIndex(root);
|
|
6693
|
+
const nodes = new Map(index.graph.nodes.map((n) => [n.id, n]));
|
|
6694
|
+
const colors = ["#b9a3ff", "#70c9a2", "#72b9e6", "#d6b879", "#e393a9", "#78c9cd"];
|
|
6695
|
+
const types = config.traceability.keys.map((k, i) => ({ ...k, color: colors[i % colors.length] }));
|
|
6696
|
+
const typesByRelation = new Map(types.map((t) => [t.relation, t]));
|
|
6665
6697
|
const edges = [];
|
|
6666
|
-
for (const edge of graph.edges
|
|
6667
|
-
|
|
6668
|
-
const
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
label: labelForNode({ ...source, ...item }),
|
|
6684
|
-
seed,
|
|
6685
|
-
ranked: Boolean(item),
|
|
6686
|
-
depth: depthByNode.get(source.id) ?? 0,
|
|
6687
|
-
nearestRoot: nearestRootByNode.get(source.id),
|
|
6688
|
-
component: 0,
|
|
6689
|
-
seededComponent: true,
|
|
6690
|
-
impactScore: seed ? 100 : item?.impactScore ?? 0,
|
|
6691
|
-
reasons: seed ? ["changed-file"] : item?.reasons ?? [],
|
|
6692
|
-
scoreBreakdown: seed ? { seed: 100 } : item?.scoreBreakdown,
|
|
6693
|
-
retrieval: item?.retrieval ?? source.retrieval,
|
|
6694
|
-
contractId: item?.contractId ?? source.contractId,
|
|
6695
|
-
title: item?.title ?? source.title,
|
|
6696
|
-
sections: item?.sections ?? source.sections,
|
|
6697
|
-
vectorEvidence: item?.vectorEvidence
|
|
6698
|
-
};
|
|
6699
|
-
});
|
|
6698
|
+
for (const edge of index.graph.edges) {
|
|
6699
|
+
const type = typesByRelation.get(edge.relation);
|
|
6700
|
+
const source = nodes.get(edge.source)?.path;
|
|
6701
|
+
const target = nodes.get(edge.target)?.path;
|
|
6702
|
+
if (!type || !source || !target || source === target) continue;
|
|
6703
|
+
if ([source, target].some((p) => resolveMemoryArea(p, config)?.scope === "local")) continue;
|
|
6704
|
+
if (!(await Promise.all([source, target].map(async (p) => {
|
|
6705
|
+
const absolute = resolve19(root, p);
|
|
6706
|
+
if (!absolute.startsWith(resolve19(root) + "/")) return false;
|
|
6707
|
+
try {
|
|
6708
|
+
return (await stat(absolute)).isFile();
|
|
6709
|
+
} catch {
|
|
6710
|
+
return false;
|
|
6711
|
+
}
|
|
6712
|
+
}))).every(Boolean)) continue;
|
|
6713
|
+
edges.push({ source, target, relation: type.relation, weight: type.weight, color: type.color });
|
|
6714
|
+
}
|
|
6700
6715
|
return {
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
nodes,
|
|
6707
|
-
edges: retainedEdges,
|
|
6708
|
-
components: retainedNodes.length > 0 ? [{ id: 0, size: retainedNodes.length, seeded: true }] : [],
|
|
6709
|
-
diagnostics: {
|
|
6710
|
-
rootNodes: retainedNodes.filter((node2) => seedIds.has(node2.id)).length,
|
|
6711
|
-
connectedNodes: retainedNodes.length,
|
|
6712
|
-
disconnectedNodesOmitted: sourceNodes.length - retainedNodes.length,
|
|
6713
|
-
maximumDepth: Math.max(0, ...depthByNode.values())
|
|
6714
|
-
},
|
|
6715
|
-
omittedRelated: impact.omittedRelated ?? 0
|
|
6716
|
+
version: 1,
|
|
6717
|
+
projectRoot: resolve19(root),
|
|
6718
|
+
config: memoryConfigSummary(config),
|
|
6719
|
+
types,
|
|
6720
|
+
edges: [...new Map(edges.map((e) => [`${e.source}\0${e.target}\0${e.relation}`, e])).values()]
|
|
6716
6721
|
};
|
|
6717
6722
|
}
|
|
6718
6723
|
|
|
6719
6724
|
// packages/cli/src/graph-view/server.mjs
|
|
6720
|
-
|
|
6721
|
-
|
|
6725
|
+
import { createServer } from "node:http";
|
|
6726
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
6727
|
+
import { dirname as dirname13, extname as extname10, join as join20, resolve as resolve20 } from "node:path";
|
|
6728
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6729
|
+
var import_node_style = __toESM(require_node_style(), 1);
|
|
6730
|
+
var ASSET_ROOT = join20(dirname13(fileURLToPath2(import.meta.url)), "assets");
|
|
6731
|
+
var CONTENT_TYPES = { ".css": "text/css; charset=utf-8", ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".cjs": "text/javascript; charset=utf-8", ".map": "application/json; charset=utf-8" };
|
|
6722
6732
|
var VENDOR_FILES = {
|
|
6723
|
-
"/vendor/sigma.js": new URL("../../node_modules/sigma/dist/sigma.min.js", import.meta.url),
|
|
6724
|
-
"/vendor/graphology.js": new URL("../../node_modules/graphology/dist/graphology.umd.min.js", import.meta.url),
|
|
6725
6733
|
"/vendor/d3.js": new URL("../../node_modules/d3/dist/d3.min.js", import.meta.url)
|
|
6726
6734
|
};
|
|
6727
6735
|
function json(response, status, value) {
|
|
@@ -6730,7 +6738,7 @@ function json(response, status, value) {
|
|
|
6730
6738
|
}
|
|
6731
6739
|
function asset(response, file) {
|
|
6732
6740
|
try {
|
|
6733
|
-
const body =
|
|
6741
|
+
const body = readFileSync21(file);
|
|
6734
6742
|
response.writeHead(200, { "content-type": CONTENT_TYPES[extname10(file)] ?? "application/octet-stream", "cache-control": "no-store" });
|
|
6735
6743
|
response.end(body);
|
|
6736
6744
|
} catch {
|
|
@@ -6738,49 +6746,42 @@ function asset(response, file) {
|
|
|
6738
6746
|
response.end("Not found");
|
|
6739
6747
|
}
|
|
6740
6748
|
}
|
|
6741
|
-
async function buildServedImpact(root, changedPaths) {
|
|
6742
|
-
const normalized = [...new Set(changedPaths.map((path) => canonicalizeChangedPath(root, path)).filter(Boolean))];
|
|
6743
|
-
const { status, index, metadata } = readFreshIndex(root);
|
|
6744
|
-
const overlay = await buildVectorImpactOverlay(root, index, normalized);
|
|
6745
|
-
const impact = buildImpactReport(index, normalized, { root, overlay, verboseSemantic: false, related: 40 });
|
|
6746
|
-
const graph = buildImpactGraphPayload(index, impact);
|
|
6747
|
-
const graphIds = new Set(graph.nodes.map((node2) => node2.id));
|
|
6748
|
-
const explorerImpact = {
|
|
6749
|
-
...impact,
|
|
6750
|
-
related: impact.related.filter((item) => graphIds.has(item.id))
|
|
6751
|
-
};
|
|
6752
|
-
return { ok: status.ok, status, metadata, impact: explorerImpact, graph };
|
|
6753
|
-
}
|
|
6754
6749
|
async function startImpactGraphServer({ root, changed, host = "127.0.0.1", port = 0 }) {
|
|
6755
|
-
|
|
6750
|
+
root = resolve20(root);
|
|
6751
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(host)) throw new Error("Graph UI is local-only; use a loopback host.");
|
|
6752
|
+
const build = async () => {
|
|
6753
|
+
const data = await exportTraceability(root);
|
|
6754
|
+
const paths = [...new Set(data.edges.flatMap((e) => [e.source, e.target]))];
|
|
6755
|
+
return { ...data, projectRoot: void 0, nodes: paths.map((path) => ({ id: path, ...import_node_style.default.describeNode(path, data.config.documentation.root) })) };
|
|
6756
|
+
};
|
|
6757
|
+
let payload = await build();
|
|
6756
6758
|
const server = createServer(async (request, response) => {
|
|
6757
6759
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? host}`);
|
|
6758
|
-
if (
|
|
6759
|
-
if (url.pathname === "/api/
|
|
6760
|
-
const paths = url.searchParams.getAll("changed");
|
|
6761
|
-
if (paths.length === 0) return json(response, 400, { ok: false, error: { code: "MISSING_CHANGED", message: "At least one changed path is required." } });
|
|
6760
|
+
if (!["GET", "HEAD"].includes(request.method)) return json(response, 405, { error: "Method not allowed" });
|
|
6761
|
+
if (url.pathname === "/api/graph") {
|
|
6762
6762
|
try {
|
|
6763
|
-
payload = await
|
|
6763
|
+
payload = await build();
|
|
6764
6764
|
return json(response, 200, payload);
|
|
6765
6765
|
} catch (error) {
|
|
6766
6766
|
return json(response, 500, { ok: false, error: { code: "GRAPH_SERVE_FAILED", message: error instanceof Error ? error.message : String(error) } });
|
|
6767
6767
|
}
|
|
6768
6768
|
}
|
|
6769
6769
|
if (VENDOR_FILES[url.pathname]) return asset(response, fileURLToPath2(VENDOR_FILES[url.pathname]));
|
|
6770
|
+
if (url.pathname === "/policy.js") return asset(response, join20(ASSET_ROOT, "../policy.cjs"));
|
|
6770
6771
|
const relative8 = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
6771
|
-
if (!["index.html", "app.css", "app.js"].includes(relative8)) {
|
|
6772
|
+
if (!["index.html", "app.css", "app.js", "file-list.js", "graph-client.js", "layout.js"].includes(relative8)) {
|
|
6772
6773
|
response.writeHead(404);
|
|
6773
6774
|
return response.end("Not found");
|
|
6774
6775
|
}
|
|
6775
|
-
return asset(response,
|
|
6776
|
+
return asset(response, join20(ASSET_ROOT, relative8));
|
|
6776
6777
|
});
|
|
6777
|
-
await new Promise((
|
|
6778
|
+
await new Promise((resolve21, reject) => {
|
|
6778
6779
|
server.once("error", reject);
|
|
6779
|
-
server.listen(port, host,
|
|
6780
|
+
server.listen(port, host, resolve21);
|
|
6780
6781
|
});
|
|
6781
6782
|
const address = server.address();
|
|
6782
6783
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
6783
|
-
return { server, url: `http://${host}:${actualPort}`, payload };
|
|
6784
|
+
return { server, url: `http://${host.includes(":") ? `[${host}]` : host}:${actualPort}`, payload };
|
|
6784
6785
|
}
|
|
6785
6786
|
|
|
6786
6787
|
// packages/cli/src/impact/format.mjs
|
|
@@ -6936,9 +6937,25 @@ async function runGraph(argv) {
|
|
|
6936
6937
|
const sub = argv[0];
|
|
6937
6938
|
const isImpact = sub === "impact";
|
|
6938
6939
|
const isServe = sub === "serve";
|
|
6939
|
-
if (!["impact", "communities", "serve"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
|
|
6940
|
+
if (!["impact", "communities", "serve", "export"].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : "Missing graph command.", "graph");
|
|
6940
6941
|
const options = parseGraphOptions(argv.slice(1));
|
|
6941
|
-
if (
|
|
6942
|
+
if (sub === "export") {
|
|
6943
|
+
const graph = await exportTraceability(options.root);
|
|
6944
|
+
delete graph.projectRoot;
|
|
6945
|
+
const directory = join21(options.root, ".dotdotgod");
|
|
6946
|
+
await mkdir(directory, { recursive: true });
|
|
6947
|
+
const path = join21(directory, "traceability.json");
|
|
6948
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
6949
|
+
try {
|
|
6950
|
+
await writeFile(temporary, JSON.stringify(graph, null, 2));
|
|
6951
|
+
await rename(temporary, path);
|
|
6952
|
+
} finally {
|
|
6953
|
+
await rm(temporary, { force: true });
|
|
6954
|
+
}
|
|
6955
|
+
console.log(options.json ? JSON.stringify({ ok: true, path, edges: graph.edges.length }) : `graph export: ${path} (${graph.edges.length} relations)`);
|
|
6956
|
+
return;
|
|
6957
|
+
}
|
|
6958
|
+
if (isServe && options.changed.length > 0) usage("graph serve now shows project traceability. Use graph impact for --changed analysis.", "graph serve");
|
|
6942
6959
|
if (isServe && !Number.isInteger(options.port)) usage("Invalid --port. Use an integer from 0 through 65535.", "graph serve");
|
|
6943
6960
|
if (isServe) {
|
|
6944
6961
|
options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
|