headlesscode 1.0.2
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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained interactive HTML visualizer for the codemap.
|
|
3
|
+
*
|
|
4
|
+
* Produces ONE html file with inline CSS + vanilla JS and the codemap JSON
|
|
5
|
+
* embedded — no external CDN, no server required: the acceptance criteria
|
|
6
|
+
* say the map must open standalone in a browser with working
|
|
7
|
+
* click-to-highlight and search. Layout is deterministic (BFS layers from
|
|
8
|
+
* root modules, order within a layer by path), so the same repo always
|
|
9
|
+
* renders the same picture.
|
|
10
|
+
*
|
|
11
|
+
* The inline JS deliberately avoids the browser's `</script>` sequence via
|
|
12
|
+
* JSON escaping, keeps all state in plain module-scope variables, and is
|
|
13
|
+
* dependency-free.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Codemap } from "./types.js"
|
|
17
|
+
|
|
18
|
+
/** Escape the embedded JSON so it can never terminate the <script> tag. */
|
|
19
|
+
function safeJson(value: Codemap): string {
|
|
20
|
+
return JSON.stringify(value).replace(/</g, "\\u003c")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderCodemapHtml(codemap: Codemap): string {
|
|
24
|
+
const json = safeJson(codemap)
|
|
25
|
+
return `<!DOCTYPE html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8">
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
30
|
+
<title>codemap — ${codemap.project}</title>
|
|
31
|
+
<style>
|
|
32
|
+
:root { color-scheme: dark; }
|
|
33
|
+
* { box-sizing: border-box; }
|
|
34
|
+
body {
|
|
35
|
+
margin: 0; background: #0f1115; color: #d6d9df;
|
|
36
|
+
font: 13px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
37
|
+
height: 100vh; display: flex; flex-direction: column; overflow: hidden;
|
|
38
|
+
}
|
|
39
|
+
header {
|
|
40
|
+
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
41
|
+
padding: 10px 16px; background: #161a22; border-bottom: 1px solid #232936;
|
|
42
|
+
}
|
|
43
|
+
header h1 { font-size: 15px; margin: 0; color: #eef1f6; font-weight: 600; }
|
|
44
|
+
header .meta { color: #8b93a3; font-size: 12px; }
|
|
45
|
+
#search {
|
|
46
|
+
flex: 1; min-width: 200px; max-width: 420px; background: #0f1115;
|
|
47
|
+
color: #d6d9df; border: 1px solid #2c3442; border-radius: 6px;
|
|
48
|
+
padding: 6px 10px; outline: none; font-size: 12px;
|
|
49
|
+
}
|
|
50
|
+
#search:focus { border-color: #5b8def; }
|
|
51
|
+
button {
|
|
52
|
+
background: #1d2430; color: #d6d9df; border: 1px solid #2c3442;
|
|
53
|
+
border-radius: 6px; padding: 5px 10px; cursor: pointer; font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
button:hover { background: #242d3d; }
|
|
56
|
+
.legend { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; font-size: 11px; color: #aab2c0; }
|
|
57
|
+
.legend .swatch { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 4px; vertical-align: -1px; }
|
|
58
|
+
main { flex: 1; display: flex; min-height: 0; }
|
|
59
|
+
#graph { flex: 1; position: relative; min-width: 0; }
|
|
60
|
+
#graph svg { width: 100%; height: 100%; display: block; cursor: grab; }
|
|
61
|
+
#graph svg.dragging { cursor: grabbing; }
|
|
62
|
+
#hint {
|
|
63
|
+
position: absolute; left: 14px; bottom: 10px; color: #6b7280; font-size: 11px;
|
|
64
|
+
pointer-events: none; user-select: none;
|
|
65
|
+
}
|
|
66
|
+
.node { cursor: pointer; }
|
|
67
|
+
.node circle { stroke: #0f1115; stroke-width: 1.5; }
|
|
68
|
+
.node text { fill: #9aa3b2; font-size: 10px; pointer-events: none; }
|
|
69
|
+
.node.dim text { fill: #4a5261; }
|
|
70
|
+
.node.dim circle { opacity: 0.25; }
|
|
71
|
+
.node.highlight circle { stroke: #fff; stroke-width: 2; }
|
|
72
|
+
.edge { stroke: #3b4352; stroke-width: 1; }
|
|
73
|
+
.edge.include { stroke: #6b4f2a; }
|
|
74
|
+
.edge.call { stroke: #4fd08e; stroke-dasharray: 5 4; }
|
|
75
|
+
.edge.dim { opacity: 0.08; }
|
|
76
|
+
.edge.highlight { stroke: #5b8def; stroke-width: 1.6; }
|
|
77
|
+
.edge.highlight.include { stroke: #e2a14f; }
|
|
78
|
+
.edge.highlight.call { stroke: #4fd08e; }
|
|
79
|
+
#detail {
|
|
80
|
+
width: 300px; border-left: 1px solid #232936; background: #141821;
|
|
81
|
+
display: none; flex-direction: column; overflow-y: auto; flex-shrink: 0;
|
|
82
|
+
}
|
|
83
|
+
#detail.open { display: flex; }
|
|
84
|
+
#detail h2 { font-size: 12px; margin: 0; padding: 12px 14px 8px; color: #eef1f6; border-bottom: 1px solid #232936; position: sticky; top: 0; background: #141821; }
|
|
85
|
+
#detail .close { float: right; cursor: pointer; color: #8b93a3; }
|
|
86
|
+
#detail .body { padding: 12px 14px; }
|
|
87
|
+
#detail .kv { display: flex; gap: 8px; margin: 3px 0; }
|
|
88
|
+
#detail .kv .k { color: #8b93a3; width: 84px; flex-shrink: 0; }
|
|
89
|
+
#detail .kv .v { color: #d6d9df; word-break: break-all; }
|
|
90
|
+
#detail h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: #8b93a3; margin: 14px 0 6px; }
|
|
91
|
+
#detail ul { margin: 0; padding-left: 2px; list-style: none; }
|
|
92
|
+
#detail li { padding: 2px 0; }
|
|
93
|
+
#detail li a { color: #5b8def; cursor: pointer; text-decoration: none; }
|
|
94
|
+
#detail li a:hover { text-decoration: underline; }
|
|
95
|
+
#detail .empty { color: #6b7280; font-style: italic; }
|
|
96
|
+
.badge { display: inline-block; padding: 1px 7px; border-radius: 9px; font-size: 10px; color: #0f1115; }
|
|
97
|
+
</style>
|
|
98
|
+
</head>
|
|
99
|
+
<body>
|
|
100
|
+
<header>
|
|
101
|
+
<h1>codemap — <span id="project"></span></h1>
|
|
102
|
+
<span class="meta" id="meta"></span>
|
|
103
|
+
<input id="search" type="search" placeholder="Filter modules by path… (e.g. engine/loop)">
|
|
104
|
+
<button id="btnFit" title="Reset zoom & pan">⌂ fit</button>
|
|
105
|
+
<button id="btnZoomIn" title="Zoom in">+</button>
|
|
106
|
+
<button id="btnZoomOut" title="Zoom out">−</button>
|
|
107
|
+
<span class="legend">
|
|
108
|
+
<span><span class="swatch" style="background:#5b8def"></span>source</span>
|
|
109
|
+
<span><span class="swatch" style="background:#46c17b"></span>test</span>
|
|
110
|
+
<span><span class="swatch" style="background:#22d3ee"></span>entrypoint</span>
|
|
111
|
+
<span><span class="swatch" style="background:#f5a524"></span>config</span>
|
|
112
|
+
<span><span class="swatch" style="background:#a78bfa"></span>vendor</span>
|
|
113
|
+
<span><span class="swatch" style="background:#6b7280"></span>generated</span>
|
|
114
|
+
<span><span class="swatch" style="background:#5b8def; width:0; height:0"></span><span style="color:#6b7280">— import</span></span>
|
|
115
|
+
<span><span class="swatch" style="background:#e2a14f; width:0; height:0"></span><span style="color:#8b6b3a">— include</span></span>
|
|
116
|
+
<span><span class="swatch" style="background:#4fd08e; width:0; height:0"></span><span style="color:#7fbf9a">-- call</span></span>
|
|
117
|
+
</span>
|
|
118
|
+
</header>
|
|
119
|
+
<main>
|
|
120
|
+
<div id="graph">
|
|
121
|
+
<svg id="svg"></svg>
|
|
122
|
+
<div id="hint">scroll to zoom · drag to pan · click a node to highlight its imports, calls and importers</div>
|
|
123
|
+
</div>
|
|
124
|
+
<aside id="detail">
|
|
125
|
+
<h2><span id="detailTitle">Module</span><span class="close" id="detailClose">✕</span></h2>
|
|
126
|
+
<div class="body" id="detailBody"></div>
|
|
127
|
+
</aside>
|
|
128
|
+
</main>
|
|
129
|
+
<script>
|
|
130
|
+
"use strict";
|
|
131
|
+
const CODEMAP = ${json};
|
|
132
|
+
|
|
133
|
+
const ROLE_COLORS = {
|
|
134
|
+
source: "#5b8def", test: "#46c17b", entrypoint: "#22d3ee",
|
|
135
|
+
config: "#f5a524", vendor: "#a78bfa", generated: "#6b7280"
|
|
136
|
+
};
|
|
137
|
+
function edgeClass(kind) {
|
|
138
|
+
return kind === "include" ? "edge include" : kind === "call" ? "edge call" : "edge";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── deterministic layout: BFS layers from root modules ─────────────────────
|
|
142
|
+
function layout(codemap) {
|
|
143
|
+
const modules = codemap.modules;
|
|
144
|
+
const byPath = new Map(modules.map((m, i) => [m.path, i]));
|
|
145
|
+
const out = modules.map(() => []);
|
|
146
|
+
const inc = modules.map(() => 0);
|
|
147
|
+
for (const e of codemap.edges) {
|
|
148
|
+
const fi = byPath.get(e.from);
|
|
149
|
+
const ti = byPath.get(e.to);
|
|
150
|
+
if (fi === undefined || ti === undefined) continue;
|
|
151
|
+
out[fi].push(ti);
|
|
152
|
+
inc[ti]++;
|
|
153
|
+
}
|
|
154
|
+
const layer = modules.map(() => -1);
|
|
155
|
+
const queue = [];
|
|
156
|
+
modules.forEach((m, i) => { if (inc[i] === 0) { layer[i] = 0; queue.push(i); } });
|
|
157
|
+
let maxLayer = 0;
|
|
158
|
+
let head = 0;
|
|
159
|
+
while (head < queue.length) {
|
|
160
|
+
const i = queue[head++];
|
|
161
|
+
maxLayer = Math.max(maxLayer, layer[i]);
|
|
162
|
+
for (const j of out[i]) {
|
|
163
|
+
if (layer[j] === -1) { layer[j] = layer[i] + 1; queue.push(j); }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Anything unreachable from roots (a pure cycle) sits at the deepest layer.
|
|
167
|
+
modules.forEach((m, i) => { if (layer[i] === -1) layer[i] = maxLayer + 1; });
|
|
168
|
+
maxLayer = Math.max(...layer);
|
|
169
|
+
|
|
170
|
+
const perLayer = new Map();
|
|
171
|
+
modules.forEach((m, i) => {
|
|
172
|
+
const l = layer[i];
|
|
173
|
+
if (!perLayer.has(l)) perLayer.set(l, []);
|
|
174
|
+
perLayer.get(l).push(i);
|
|
175
|
+
});
|
|
176
|
+
// Fixed logical canvas width: spreading every layer across its own width
|
|
177
|
+
// makes the widest layer set the canvas size, which fit() then shrinks to
|
|
178
|
+
// sub-pixel dots — a "blank" graph. A fixed width keeps the initial fit
|
|
179
|
+
// legible; over-dense layers simply overlap and the zoom/search reveal
|
|
180
|
+
// them (deliberate v1 tradeoff).
|
|
181
|
+
const W = 2400;
|
|
182
|
+
const ROW = 110;
|
|
183
|
+
const H = (maxLayer + 1) * ROW + 80;
|
|
184
|
+
const positions = modules.map(() => ({ x: 0, y: 0 }));
|
|
185
|
+
for (const [l, arr] of perLayer) {
|
|
186
|
+
const span = W - 120;
|
|
187
|
+
const step = arr.length > 1 ? span / (arr.length - 1) : 0;
|
|
188
|
+
arr.forEach((i, k) => {
|
|
189
|
+
positions[i] = { x: 60 + k * step, y: 60 + l * ROW };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return { W, H, positions, layer };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── render ─────────────────────────────────────────────────────────────────
|
|
196
|
+
const svg = document.getElementById("svg");
|
|
197
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
198
|
+
const { W, H, positions, layer } = layout(CODEMAP);
|
|
199
|
+
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
|
|
200
|
+
const viewport = document.createElementNS(NS, "g");
|
|
201
|
+
viewport.setAttribute("id", "viewport");
|
|
202
|
+
svg.appendChild(viewport);
|
|
203
|
+
|
|
204
|
+
const byPath = new Map(CODEMAP.modules.map((m, i) => [m.path, i]));
|
|
205
|
+
const nodeEls = [];
|
|
206
|
+
const labelEls = [];
|
|
207
|
+
const edgeEls = [];
|
|
208
|
+
const incoming = CODEMAP.modules.map(() => []);
|
|
209
|
+
const outgoing = CODEMAP.modules.map(() => []);
|
|
210
|
+
const importIn = CODEMAP.modules.map(() => []);
|
|
211
|
+
const importOut = CODEMAP.modules.map(() => []);
|
|
212
|
+
const callIn = CODEMAP.modules.map(() => []);
|
|
213
|
+
const callOut = CODEMAP.modules.map(() => []);
|
|
214
|
+
|
|
215
|
+
for (const e of CODEMAP.edges) {
|
|
216
|
+
const fi = byPath.get(e.from);
|
|
217
|
+
const ti = byPath.get(e.to);
|
|
218
|
+
if (fi === undefined || ti === undefined) continue;
|
|
219
|
+
outgoing[fi].push(ti);
|
|
220
|
+
incoming[ti].push(fi);
|
|
221
|
+
if (e.kind === "call") {
|
|
222
|
+
callOut[fi].push(ti);
|
|
223
|
+
callIn[ti].push(fi);
|
|
224
|
+
} else {
|
|
225
|
+
importOut[fi].push(ti);
|
|
226
|
+
importIn[ti].push(fi);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// edges first (under nodes)
|
|
231
|
+
for (const e of CODEMAP.edges) {
|
|
232
|
+
const fi = byPath.get(e.from);
|
|
233
|
+
const ti = byPath.get(e.to);
|
|
234
|
+
if (fi === undefined || ti === undefined) continue;
|
|
235
|
+
const a = positions[fi];
|
|
236
|
+
const b = positions[ti];
|
|
237
|
+
const line = document.createElementNS(NS, "line");
|
|
238
|
+
line.setAttribute("x1", a.x); line.setAttribute("y1", a.y);
|
|
239
|
+
line.setAttribute("x2", b.x); line.setAttribute("y2", b.y);
|
|
240
|
+
line.setAttribute("class", edgeClass(e.kind));
|
|
241
|
+
line.setAttribute("data-kind", e.kind);
|
|
242
|
+
line.setAttribute("data-from", e.from);
|
|
243
|
+
line.setAttribute("data-to", e.to);
|
|
244
|
+
viewport.appendChild(line);
|
|
245
|
+
edgeEls.push(line);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// nodes
|
|
249
|
+
CODEMAP.modules.forEach((m, i) => {
|
|
250
|
+
const g = document.createElementNS(NS, "g");
|
|
251
|
+
g.setAttribute("class", "node");
|
|
252
|
+
g.setAttribute("data-path", m.path);
|
|
253
|
+
g.setAttribute("transform", "translate(" + positions[i].x + "," + positions[i].y + ")");
|
|
254
|
+
const circle = document.createElementNS(NS, "circle");
|
|
255
|
+
circle.setAttribute("r", 7);
|
|
256
|
+
circle.setAttribute("fill", ROLE_COLORS[m.role] || "#5b8def");
|
|
257
|
+
const label = document.createElementNS(NS, "text");
|
|
258
|
+
label.setAttribute("x", 10);
|
|
259
|
+
label.setAttribute("y", 3);
|
|
260
|
+
label.textContent = m.path.split("/").pop() || m.path;
|
|
261
|
+
const title = document.createElementNS(NS, "title");
|
|
262
|
+
title.textContent = m.path + " [" + m.role + "]";
|
|
263
|
+
g.appendChild(circle);
|
|
264
|
+
g.appendChild(label);
|
|
265
|
+
g.appendChild(title);
|
|
266
|
+
g.addEventListener("click", (ev) => { ev.stopPropagation(); selectNode(i); });
|
|
267
|
+
viewport.appendChild(g);
|
|
268
|
+
nodeEls.push(g);
|
|
269
|
+
labelEls.push(label);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ── zoom / pan ─────────────────────────────────────────────────────────────
|
|
273
|
+
let k = 1, tx = 0, ty = 0, dragging = false, lastX = 0, lastY = 0;
|
|
274
|
+
// Labels only pay for themselves when zoomed in far enough to read them;
|
|
275
|
+
// below that they are pure clutter (esp. on 2k+ node repos).
|
|
276
|
+
const LABEL_MIN_SCALE = 0.5;
|
|
277
|
+
function applyTransform() {
|
|
278
|
+
viewport.setAttribute("transform", "translate(" + tx + "," + ty + ") scale(" + k + ")");
|
|
279
|
+
const show = k >= LABEL_MIN_SCALE;
|
|
280
|
+
labelEls.forEach((el) => { el.style.display = show ? "" : "none"; });
|
|
281
|
+
}
|
|
282
|
+
function fit() {
|
|
283
|
+
const rect = svg.getBoundingClientRect();
|
|
284
|
+
k = Math.min(rect.width / W, rect.height / H);
|
|
285
|
+
// Fit-to-tiny is worse than no fit: never shrink below legibility.
|
|
286
|
+
k = Math.max(0.4, Math.min(k, 1));
|
|
287
|
+
tx = (rect.width - W * k) / 2;
|
|
288
|
+
ty = (rect.height - H * k) / 2;
|
|
289
|
+
applyTransform();
|
|
290
|
+
}
|
|
291
|
+
svg.addEventListener("wheel", (ev) => {
|
|
292
|
+
ev.preventDefault();
|
|
293
|
+
const rect = svg.getBoundingClientRect();
|
|
294
|
+
const mx = ev.clientX - rect.left, my = ev.clientY - rect.top;
|
|
295
|
+
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
|
|
296
|
+
const nk = Math.max(0.02, Math.min(k * factor, 8));
|
|
297
|
+
tx = mx - ((mx - tx) * nk) / k;
|
|
298
|
+
ty = my - ((my - ty) * nk) / k;
|
|
299
|
+
k = nk;
|
|
300
|
+
applyTransform();
|
|
301
|
+
}, { passive: false });
|
|
302
|
+
svg.addEventListener("mousedown", (ev) => {
|
|
303
|
+
if (ev.button !== 0) return;
|
|
304
|
+
dragging = true; lastX = ev.clientX; lastY = ev.clientY;
|
|
305
|
+
svg.classList.add("dragging");
|
|
306
|
+
});
|
|
307
|
+
window.addEventListener("mousemove", (ev) => {
|
|
308
|
+
if (!dragging) return;
|
|
309
|
+
tx += ev.clientX - lastX; ty += ev.clientY - lastY;
|
|
310
|
+
lastX = ev.clientX; lastY = ev.clientY;
|
|
311
|
+
applyTransform();
|
|
312
|
+
});
|
|
313
|
+
window.addEventListener("mouseup", () => { dragging = false; svg.classList.remove("dragging"); });
|
|
314
|
+
document.getElementById("btnFit").addEventListener("click", fit);
|
|
315
|
+
document.getElementById("btnZoomIn").addEventListener("click", () => {
|
|
316
|
+
k = Math.min(k * 1.3, 8); applyTransform();
|
|
317
|
+
});
|
|
318
|
+
document.getElementById("btnZoomOut").addEventListener("click", () => {
|
|
319
|
+
k = Math.max(k / 1.3, 0.02); applyTransform();
|
|
320
|
+
});
|
|
321
|
+
fit();
|
|
322
|
+
|
|
323
|
+
// ── selection / highlight ──────────────────────────────────────────────────
|
|
324
|
+
const detail = document.getElementById("detail");
|
|
325
|
+
function clearHighlight() {
|
|
326
|
+
edgeEls.forEach((el) => el.setAttribute("class", edgeClass(el.getAttribute("data-kind"))));
|
|
327
|
+
nodeEls.forEach((el) => el.classList.remove("dim", "highlight"));
|
|
328
|
+
}
|
|
329
|
+
function selectNode(i) {
|
|
330
|
+
clearHighlight();
|
|
331
|
+
const path = CODEMAP.modules[i].path;
|
|
332
|
+
const connected = new Set([i]);
|
|
333
|
+
for (const j of incoming[i]) connected.add(j);
|
|
334
|
+
for (const j of outgoing[i]) connected.add(j);
|
|
335
|
+
edgeEls.forEach((el) => {
|
|
336
|
+
const f = byPath.get(el.getAttribute("data-from"));
|
|
337
|
+
const t = byPath.get(el.getAttribute("data-to"));
|
|
338
|
+
const active = (f === i && connected.has(t)) || (t === i && connected.has(f));
|
|
339
|
+
if (active) {
|
|
340
|
+
el.classList.add("highlight");
|
|
341
|
+
} else {
|
|
342
|
+
el.classList.add("dim");
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
nodeEls.forEach((el, j) => {
|
|
346
|
+
const p = el.getAttribute("data-path");
|
|
347
|
+
if (j === i) { el.classList.add("highlight"); return; }
|
|
348
|
+
if (connected.has(j)) { el.classList.add("highlight"); }
|
|
349
|
+
else { el.classList.add("dim"); }
|
|
350
|
+
});
|
|
351
|
+
showDetail(i);
|
|
352
|
+
}
|
|
353
|
+
function showDetail(i) {
|
|
354
|
+
const m = CODEMAP.modules[i];
|
|
355
|
+
document.getElementById("detailTitle").textContent = m.path.split("/").pop();
|
|
356
|
+
const color = ROLE_COLORS[m.role] || "#5b8def";
|
|
357
|
+
const ext = (CODEMAP.externalDeps && CODEMAP.externalDeps[m.path]) || [];
|
|
358
|
+
let html = "";
|
|
359
|
+
html += '<div class="kv"><span class="k">path</span><span class="v">' + escapeHtml(m.path) + "</span></div>";
|
|
360
|
+
html += '<div class="kv"><span class="k">role</span><span class="v"><span class="badge" style="background:' + color + '">' + m.role + "</span></span></div>";
|
|
361
|
+
html += '<div class="kv"><span class="k">language</span><span class="v">' + m.language + "</span></div>";
|
|
362
|
+
html += '<div class="kv"><span class="k">size</span><span class="v">' + m.sizeBytes + " bytes · " + m.lineCount + " lines</span></div>";
|
|
363
|
+
html += '<div class="kv"><span class="k">layer</span><span class="v">' + layer[i] + "</span></div>";
|
|
364
|
+
if (ext.length) {
|
|
365
|
+
html += '<h3>external deps (' + ext.length + ")</h3><ul>";
|
|
366
|
+
html += ext.slice(0, 40).map((s) => "<li>" + escapeHtml(s) + "</li>").join("");
|
|
367
|
+
if (ext.length > 40) html += "<li>…</li>";
|
|
368
|
+
html += "</ul>";
|
|
369
|
+
}
|
|
370
|
+
html += "<h3>imports (" + importOut[i].length + ")</h3><ul>";
|
|
371
|
+
html += importOut[i].length
|
|
372
|
+
? importOut[i].map((p) => '<li><a data-jump="' + CODEMAP.modules[p].path + '">' + escapeHtml(CODEMAP.modules[p].path) + "</a></li>").join("")
|
|
373
|
+
: '<li class="empty">none</li>';
|
|
374
|
+
html += "</ul>";
|
|
375
|
+
html += "<h3>imported by (" + importIn[i].length + ")</h3><ul>";
|
|
376
|
+
html += importIn[i].length
|
|
377
|
+
? importIn[i].map((p) => '<li><a data-jump="' + CODEMAP.modules[p].path + '">' + escapeHtml(CODEMAP.modules[p].path) + "</a></li>").join("")
|
|
378
|
+
: '<li class="empty">none</li>';
|
|
379
|
+
html += "</ul>";
|
|
380
|
+
html += "<h3>calls (" + callOut[i].length + ")</h3><ul>";
|
|
381
|
+
html += callOut[i].length
|
|
382
|
+
? callOut[i].map((p) => '<li><a data-jump="' + CODEMAP.modules[p].path + '">' + escapeHtml(CODEMAP.modules[p].path) + "</a></li>").join("")
|
|
383
|
+
: '<li class="empty">none</li>';
|
|
384
|
+
html += "</ul>";
|
|
385
|
+
html += "<h3>called by (" + callIn[i].length + ")</h3><ul>";
|
|
386
|
+
html += callIn[i].length
|
|
387
|
+
? callIn[i].map((p) => '<li><a data-jump="' + CODEMAP.modules[p].path + '">' + escapeHtml(CODEMAP.modules[p].path) + "</a></li>").join("")
|
|
388
|
+
: '<li class="empty">none</li>';
|
|
389
|
+
html += "</ul>";
|
|
390
|
+
const flows = (CODEMAP.flows && CODEMAP.flows[m.path]) || [];
|
|
391
|
+
if (m.role === "entrypoint" && flows.length) {
|
|
392
|
+
html += "<h3>flow from this entrypoint (" + flows.length + ")</h3><ul>";
|
|
393
|
+
html += flows.slice(0, 40).map((p) => '<li><a data-jump="' + p + '">' + escapeHtml(p) + "</a></li>").join("");
|
|
394
|
+
if (flows.length > 40) html += "<li>…</li>";
|
|
395
|
+
html += "</ul>";
|
|
396
|
+
}
|
|
397
|
+
document.getElementById("detailBody").innerHTML = html;
|
|
398
|
+
detail.classList.add("open");
|
|
399
|
+
detail.querySelectorAll("a[data-jump]").forEach((a) => {
|
|
400
|
+
a.addEventListener("click", () => {
|
|
401
|
+
const target = a.getAttribute("data-jump");
|
|
402
|
+
const j = byPath.get(target);
|
|
403
|
+
if (j !== undefined) selectNode(j);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function escapeHtml(s) {
|
|
408
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
409
|
+
}
|
|
410
|
+
document.getElementById("detailClose").addEventListener("click", () => {
|
|
411
|
+
detail.classList.remove("open");
|
|
412
|
+
clearHighlight();
|
|
413
|
+
});
|
|
414
|
+
svg.addEventListener("click", () => {
|
|
415
|
+
detail.classList.remove("open");
|
|
416
|
+
clearHighlight();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ── search / filter ────────────────────────────────────────────────────────
|
|
420
|
+
const search = document.getElementById("search");
|
|
421
|
+
let lastQuery = "";
|
|
422
|
+
function applyFilter() {
|
|
423
|
+
const q = search.value.trim().toLowerCase();
|
|
424
|
+
if (q === lastQuery) return;
|
|
425
|
+
lastQuery = q;
|
|
426
|
+
const visible = new Set();
|
|
427
|
+
CODEMAP.modules.forEach((m, i) => {
|
|
428
|
+
if (!q || m.path.toLowerCase().includes(q)) visible.add(i);
|
|
429
|
+
});
|
|
430
|
+
nodeEls.forEach((el, i) => {
|
|
431
|
+
el.style.display = visible.has(i) ? "" : "none";
|
|
432
|
+
});
|
|
433
|
+
edgeEls.forEach((el) => {
|
|
434
|
+
const f = byPath.get(el.getAttribute("data-from"));
|
|
435
|
+
const t = byPath.get(el.getAttribute("data-to"));
|
|
436
|
+
el.style.display = visible.has(f) && visible.has(t) ? "" : "none";
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
search.addEventListener("input", applyFilter);
|
|
440
|
+
applyFilter();
|
|
441
|
+
|
|
442
|
+
// ── header meta ────────────────────────────────────────────────────────────
|
|
443
|
+
document.getElementById("project").textContent = CODEMAP.project;
|
|
444
|
+
const edgeCount = CODEMAP.edges.length;
|
|
445
|
+
document.getElementById("meta").textContent =
|
|
446
|
+
CODEMAP.modules.length + " modules · " + edgeCount + " edges · fp " + CODEMAP.fingerprint.slice(0, 12) + " · " + CODEMAP.generatedAt;
|
|
447
|
+
</script>
|
|
448
|
+
</body>
|
|
449
|
+
</html>
|
|
450
|
+
`
|
|
451
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codemap storage in the CENTRAL per-project data store — mirroring the
|
|
3
|
+
* codesearch precedent exactly: `<dataRoot>/projects/<project-key>/codemap/`
|
|
4
|
+
* (see src/project-store.ts). Worktrees of a repo share one codemap for free,
|
|
5
|
+
* and the dashboard reads it from the same resolved location.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs"
|
|
9
|
+
import * as path from "node:path"
|
|
10
|
+
|
|
11
|
+
import { resolveProjectDataDir } from "../project-store.js"
|
|
12
|
+
import type { Codemap, CodemapLock } from "./types.js"
|
|
13
|
+
|
|
14
|
+
/** Directory holding a project's codemap artifacts, relative to the project key dir. */
|
|
15
|
+
export const CODEMAP_DIR_NAME = "codemap"
|
|
16
|
+
|
|
17
|
+
/** Workspace root -> absolute codemap storage dir (central project store). */
|
|
18
|
+
export function codemapDir(workspaceRoot: string): string {
|
|
19
|
+
return path.join(resolveProjectDataDir(workspaceRoot), CODEMAP_DIR_NAME)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function codemapJsonPath(workspaceRoot: string): string {
|
|
23
|
+
return path.join(codemapDir(workspaceRoot), "codemap.json")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function codemapLockPath(workspaceRoot: string): string {
|
|
27
|
+
return path.join(codemapDir(workspaceRoot), "codemap.lock")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function codemapHtmlPath(workspaceRoot: string): string {
|
|
31
|
+
return path.join(codemapDir(workspaceRoot), "codemap.html")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Loose-parse the stored lock; undefined when missing/unreadable/malformed. */
|
|
35
|
+
export function loadLock(workspaceRoot: string): CodemapLock | undefined {
|
|
36
|
+
let raw: string
|
|
37
|
+
try {
|
|
38
|
+
raw = fs.readFileSync(codemapLockPath(workspaceRoot), "utf-8")
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
44
|
+
if (typeof parsed.fingerprints !== "object" || parsed.fingerprints === null) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
const fingerprints: Record<string, string> = {}
|
|
48
|
+
for (const [k, v] of Object.entries(parsed.fingerprints)) {
|
|
49
|
+
if (typeof v === "string") {
|
|
50
|
+
fingerprints[k] = v
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
project: typeof parsed.project === "string" ? parsed.project : "",
|
|
55
|
+
fingerprints,
|
|
56
|
+
codemapFingerprint: typeof parsed.codemapFingerprint === "string" ? parsed.codemapFingerprint : "",
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Load the stored codemap.json; undefined when missing/unreadable/malformed. */
|
|
64
|
+
export function loadCodemap(workspaceRoot: string): Codemap | undefined {
|
|
65
|
+
let raw: string
|
|
66
|
+
try {
|
|
67
|
+
raw = fs.readFileSync(codemapJsonPath(workspaceRoot), "utf-8")
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(raw) as Codemap
|
|
73
|
+
if (!Array.isArray(parsed.modules) || !Array.isArray(parsed.edges) || typeof parsed.fingerprint !== "string") {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
return parsed
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the deterministic per-project codemap subsystem
|
|
3
|
+
* (src/codemap/ — module inventory, import/include/call graph,
|
|
4
|
+
* entrypoint-rooted flows, self-contained HTML visualizer,
|
|
5
|
+
* fingerprint-aware watcher, dashboard endpoints).
|
|
6
|
+
*
|
|
7
|
+
* Everything here is produced by a SCRIPT, never an LLM: the whole point
|
|
8
|
+
* (issue #17/#18) is a worker that needs to know "what is this repo, what
|
|
9
|
+
* imports what, what calls what, where do the entrypoints lead" can read
|
|
10
|
+
* codemap.json instead of re-deriving structure via grep every session —
|
|
11
|
+
* and trust it because it was generated mechanically.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Naming-convention heuristic for a module's role (no semantics needed). */
|
|
15
|
+
export type ModuleRole = "source" | "test" | "entrypoint" | "config" | "vendor" | "generated"
|
|
16
|
+
|
|
17
|
+
/** Coarse language family, used by the extractor + visualizer. */
|
|
18
|
+
export type ModuleLanguage = "typescript" | "javascript" | "python" | "cpp" | "c" | "other"
|
|
19
|
+
|
|
20
|
+
/** One module (file) in the map. */
|
|
21
|
+
export interface ModuleEntry {
|
|
22
|
+
/** Workspace-relative POSIX path (e.g. "src/engine/loop.ts"). */
|
|
23
|
+
path: string
|
|
24
|
+
language: ModuleLanguage
|
|
25
|
+
role: ModuleRole
|
|
26
|
+
/** File size in bytes (for a size/role heuristic). */
|
|
27
|
+
sizeBytes: number
|
|
28
|
+
/** Line count (for the same heuristic + the visualizer's detail panel). */
|
|
29
|
+
lineCount: number
|
|
30
|
+
/** sha256 hex of the file content — the fingerprint key for the lock. */
|
|
31
|
+
hash: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One directed edge between two workspace modules. Only edges whose target
|
|
36
|
+
* resolves INSIDE the workspace appear here; edges that resolve outside
|
|
37
|
+
* (node_modules, builtins, absolute paths, failures) are summarized per
|
|
38
|
+
* module in ModuleEntry.externalDeps so the map never explodes with
|
|
39
|
+
* dependency nodes.
|
|
40
|
+
*/
|
|
41
|
+
export interface CodemapEdge {
|
|
42
|
+
/** Workspace-relative POSIX path of the source module. */
|
|
43
|
+
from: string
|
|
44
|
+
/** Workspace-relative POSIX path of the target module. */
|
|
45
|
+
to: string
|
|
46
|
+
/**
|
|
47
|
+
* "import" for TS/JS/Python import statements, "include" for C/C++,
|
|
48
|
+
* "call" for TS/JS cross-module function/method/constructor calls
|
|
49
|
+
* (Phase 2 — resolved via the TS checker, not text matching).
|
|
50
|
+
*/
|
|
51
|
+
kind: "import" | "include" | "call"
|
|
52
|
+
/**
|
|
53
|
+
* Raw specifier as written in source (e.g. "./b.js", "a.b.c", "foo.h").
|
|
54
|
+
* For call edges: the sorted, comma-joined callee names that make up the
|
|
55
|
+
* edge (one edge per from/to pair, so the module graph stays clean).
|
|
56
|
+
*/
|
|
57
|
+
specifier: string
|
|
58
|
+
/** True for TS `import type` / `export type` edges (thinner in the view). */
|
|
59
|
+
isTypeOnly?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The full codemap document (`codemap.json`). */
|
|
63
|
+
export interface Codemap {
|
|
64
|
+
/** Human-readable project name (repo basename, or the workspace basename). */
|
|
65
|
+
project: string
|
|
66
|
+
/** Resolved absolute workspace root the map was generated from. */
|
|
67
|
+
root: string
|
|
68
|
+
/** ISO timestamp of generation. */
|
|
69
|
+
generatedAt: string
|
|
70
|
+
/**
|
|
71
|
+
* Deterministic content hash of the map: sha256 over the sorted module
|
|
72
|
+
* paths + hashes and the sorted edges. Stable across regenerations of an
|
|
73
|
+
* unchanged repo, so consumers can cheaply compare "did anything change".
|
|
74
|
+
*/
|
|
75
|
+
fingerprint: string
|
|
76
|
+
modules: ModuleEntry[]
|
|
77
|
+
edges: CodemapEdge[]
|
|
78
|
+
/**
|
|
79
|
+
* Per-module deduplicated list of import specifiers that did NOT resolve
|
|
80
|
+
* to a workspace-internal module (node_modules, builtins, failures). Null
|
|
81
|
+
* (not empty) on a module with no such imports.
|
|
82
|
+
*/
|
|
83
|
+
externalDeps: Record<string, string[]>
|
|
84
|
+
/**
|
|
85
|
+
* Entrypoint-rooted end-to-end flows (Phase 2, issue #18): for each
|
|
86
|
+
* module whose role is "entrypoint", the sorted list of every module
|
|
87
|
+
* reachable from it along directed import+call edges. Derived, not
|
|
88
|
+
* fingerprinted — a pure function of modules + edges. Deliberately NOT
|
|
89
|
+
* ranked by "importance" (see src/codemap/flows.ts for why).
|
|
90
|
+
*/
|
|
91
|
+
flows: Record<string, string[]>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Per-module content fingerprints — the change-detection lock. */
|
|
95
|
+
export interface CodemapLock {
|
|
96
|
+
project: string
|
|
97
|
+
/** Workspace-relative POSIX path -> sha256 hex of the file content. */
|
|
98
|
+
fingerprints: Record<string, string>
|
|
99
|
+
/** The fingerprint of the codemap.json this lock was written alongside. */
|
|
100
|
+
codemapFingerprint: string
|
|
101
|
+
}
|