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,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge extraction for the codemap — one mechanical extractor per language:
|
|
3
|
+
*
|
|
4
|
+
* - TypeScript/JS: reuse `getImportGraph()` from src/codeintel/import-graph.ts
|
|
5
|
+
* DIRECTLY (built on the TS Language Service, resolves tsconfig `paths`
|
|
6
|
+
* aliases exactly as the compiler would) — issue #17 explicitly says to
|
|
7
|
+
* wire it in, not reimplement it — plus `getCallGraph()` from
|
|
8
|
+
* src/codeintel/call-graph.ts for Phase 2's cross-module call edges
|
|
9
|
+
* (issue #18: real checker symbol resolution, never text matching).
|
|
10
|
+
* Modules the TS program doesn't cover (e.g. standalone .mjs scripts
|
|
11
|
+
* outside tsconfig's include) simply carry no import/call edges.
|
|
12
|
+
* - Python: shell out to scripts/codemap-python-extract.py (stdlib `ast`
|
|
13
|
+
* via a small Python subprocess — the project's established pattern for
|
|
14
|
+
* Python tooling; the script is deterministic, no LLM). Call edges stay
|
|
15
|
+
* Phase 2 for Python (per-framework pattern lists — FastAPI routes,
|
|
16
|
+
* Celery tasks — are project-specific by design, see docs/codemap.md).
|
|
17
|
+
* - C/C++: regex over `#include` directives (mechanical, good enough for
|
|
18
|
+
* structure; symbol-level resolution is explicitly Phase 2 and likely
|
|
19
|
+
* stays includes-only — a real call graph needs a compiler frontend).
|
|
20
|
+
*
|
|
21
|
+
* Edges whose target resolves OUTSIDE the workspace (node_modules, builtins,
|
|
22
|
+
* absolute paths) are NOT emitted as graph edges — they are summarized per
|
|
23
|
+
* module in `externalDeps` so the map stays a map of THIS project.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from "node:fs"
|
|
27
|
+
import * as path from "node:path"
|
|
28
|
+
import { spawn } from "node:child_process"
|
|
29
|
+
|
|
30
|
+
import { getCallGraph } from "../codeintel/call-graph.js"
|
|
31
|
+
import { getImportGraph } from "../codeintel/import-graph.js"
|
|
32
|
+
import { getCodeIntelCache } from "../codeintel/program.js"
|
|
33
|
+
import type { CodemapEdge, ModuleEntry } from "./types.js"
|
|
34
|
+
|
|
35
|
+
/** Absolute path of the Python AST extractor script (env-overridable for tests). */
|
|
36
|
+
export function pythonExtractorScript(): string {
|
|
37
|
+
const override = process.env.HEADLESSCODE_CODEMAP_PYTHON_SCRIPT?.trim()
|
|
38
|
+
if (override) {
|
|
39
|
+
return override
|
|
40
|
+
}
|
|
41
|
+
return path.resolve(new URL("../../scripts/codemap-python-extract.py", import.meta.url).pathname)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** C/C++ `#include` directive: `#include <foo.h>` or `#include "foo.h"`. */
|
|
45
|
+
const INCLUDE_RE = /^\s*#\s*include\s*[<"]([^>"]+)[>"]/gm
|
|
46
|
+
|
|
47
|
+
/** True when `rel` (workspace-relative posix) stays inside the workspace. */
|
|
48
|
+
function isInsideWorkspace(rel: string): boolean {
|
|
49
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** abs path -> workspace-relative posix path (undefined when outside). */
|
|
53
|
+
function toWorkspaceRel(root: string, abs: string): string | undefined {
|
|
54
|
+
const rel = path.relative(root, abs).split(path.sep).join("/")
|
|
55
|
+
return isInsideWorkspace(rel) ? rel : undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Collect TS/JS import edges from the shared codeintel import graph. */
|
|
59
|
+
function extractTypeScriptEdges(root: string, modules: ModuleEntry[]): { edges: CodemapEdge[]; externalDeps: Record<string, string[]> } {
|
|
60
|
+
const moduleSet = new Set(modules.map((m) => m.path))
|
|
61
|
+
const edges: CodemapEdge[] = []
|
|
62
|
+
const externalDeps: Record<string, string[]> = {}
|
|
63
|
+
|
|
64
|
+
// Skip program loading entirely on non-TS projects (a Python-only repo
|
|
65
|
+
// must not pay for an empty TS program).
|
|
66
|
+
if (!modules.some((m) => m.language === "typescript" || m.language === "javascript")) {
|
|
67
|
+
return { edges, externalDeps }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let intel
|
|
71
|
+
try {
|
|
72
|
+
intel = getCodeIntelCache(root).get()
|
|
73
|
+
} catch {
|
|
74
|
+
// A broken/absent tsconfig must not kill the whole codemap: the module
|
|
75
|
+
// inventory + other languages still work.
|
|
76
|
+
return { edges, externalDeps }
|
|
77
|
+
}
|
|
78
|
+
const graph = getImportGraph(intel)
|
|
79
|
+
const callGraph = getCallGraph(intel)
|
|
80
|
+
|
|
81
|
+
for (const module of modules) {
|
|
82
|
+
if (module.language !== "typescript" && module.language !== "javascript") {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
const abs = path.join(root, module.path)
|
|
86
|
+
const importEdges = graph.imports.get(abs)
|
|
87
|
+
if (importEdges === undefined || importEdges.length === 0) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const externals = new Set<string>()
|
|
91
|
+
for (const edge of importEdges) {
|
|
92
|
+
if (edge.resolved === undefined) {
|
|
93
|
+
externals.add(edge.specifier)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
const rel = toWorkspaceRel(root, edge.resolved)
|
|
97
|
+
if (rel === undefined || !moduleSet.has(rel)) {
|
|
98
|
+
externals.add(edge.specifier)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
edges.push({
|
|
102
|
+
from: module.path,
|
|
103
|
+
to: rel,
|
|
104
|
+
kind: "import",
|
|
105
|
+
specifier: edge.specifier,
|
|
106
|
+
isTypeOnly: edge.isTypeOnly === true,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
if (externals.size > 0) {
|
|
110
|
+
externalDeps[module.path] = [...externals].sort()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Phase 2 call edges (issue #18): cross-module function/method/constructor
|
|
115
|
+
// calls, resolved by the TS checker to the callee's defining file. One
|
|
116
|
+
// edge per from/to pair — callee names are joined into `specifier` so the
|
|
117
|
+
// module graph stays clean (no parallel lines in the visualizer).
|
|
118
|
+
const CALL_NAMES_CAP = 8
|
|
119
|
+
for (const module of modules) {
|
|
120
|
+
if (module.language !== "typescript" && module.language !== "javascript") {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
const abs = path.join(root, module.path)
|
|
124
|
+
const calls = callGraph.get(abs)
|
|
125
|
+
if (calls === undefined || calls.length === 0) {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
const byTarget = new Map<string, string[]>()
|
|
129
|
+
for (const call of calls) {
|
|
130
|
+
if (call.resolvedFile === undefined) {
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
const rel = toWorkspaceRel(root, call.resolvedFile)
|
|
134
|
+
if (rel === undefined || rel === module.path || !moduleSet.has(rel)) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
const names = byTarget.get(rel) ?? []
|
|
138
|
+
if (!names.includes(call.callee)) {
|
|
139
|
+
names.push(call.callee)
|
|
140
|
+
}
|
|
141
|
+
byTarget.set(rel, names)
|
|
142
|
+
}
|
|
143
|
+
for (const [target, names] of byTarget) {
|
|
144
|
+
names.sort()
|
|
145
|
+
const shown = names.length > CALL_NAMES_CAP ? [...names.slice(0, CALL_NAMES_CAP), `… (${names.length} callees)`] : names
|
|
146
|
+
edges.push({ from: module.path, to: target, kind: "call", specifier: shown.join(", ") })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { edges, externalDeps }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Run the Python AST extractor and emit import edges. */
|
|
153
|
+
async function extractPythonEdges(root: string, modules: ModuleEntry[]): Promise<{ edges: CodemapEdge[]; externalDeps: Record<string, string[]> }> {
|
|
154
|
+
const pyModules = modules.filter((m) => m.language === "python")
|
|
155
|
+
const edges: CodemapEdge[] = []
|
|
156
|
+
const externalDeps: Record<string, string[]> = {}
|
|
157
|
+
if (pyModules.length === 0) {
|
|
158
|
+
return { edges, externalDeps }
|
|
159
|
+
}
|
|
160
|
+
const moduleSet = new Set(modules.map((m) => m.path))
|
|
161
|
+
|
|
162
|
+
// Run the extractor with the payload on stdin (spawn — execFile has no
|
|
163
|
+
// `input` option, and the file list can be thousands of entries).
|
|
164
|
+
const stdout = await runPythonExtractor(root, pyModules.map((m) => m.path))
|
|
165
|
+
if (stdout === undefined) {
|
|
166
|
+
// python3 missing or the script failed — non-fatal: log and continue
|
|
167
|
+
// with the rest of the map (matches the project's non-fatal pattern
|
|
168
|
+
// for auxiliary subsystems). The inventory still lists the modules.
|
|
169
|
+
return { edges, externalDeps }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let parsed: { files?: Record<string, Array<{ specifier: string; kind: string; resolved: string | null }>> }
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(stdout)
|
|
175
|
+
} catch {
|
|
176
|
+
console.warn("[codemap] python extractor returned malformed output — skipped")
|
|
177
|
+
return { edges, externalDeps }
|
|
178
|
+
}
|
|
179
|
+
for (const [file, imports] of Object.entries(parsed.files ?? {})) {
|
|
180
|
+
const externals = new Set<string>()
|
|
181
|
+
for (const imp of imports) {
|
|
182
|
+
if (imp.resolved === null) {
|
|
183
|
+
externals.add(imp.specifier)
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
if (!moduleSet.has(imp.resolved)) {
|
|
187
|
+
externals.add(imp.specifier)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
edges.push({ from: file, to: imp.resolved, kind: "import", specifier: imp.specifier })
|
|
191
|
+
}
|
|
192
|
+
if (externals.size > 0) {
|
|
193
|
+
externalDeps[file] = [...externals].sort()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { edges, externalDeps }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Regex-based C/C++ `#include` edges (best-effort path resolution). */
|
|
200
|
+
function extractCppEdges(root: string, modules: ModuleEntry[]): { edges: CodemapEdge[]; externalDeps: Record<string, string[]> } {
|
|
201
|
+
const cppModules = modules.filter((m) => m.language === "cpp" || m.language === "c")
|
|
202
|
+
const edges: CodemapEdge[] = []
|
|
203
|
+
const externalDeps: Record<string, string[]> = {}
|
|
204
|
+
if (cppModules.length === 0) {
|
|
205
|
+
return { edges, externalDeps }
|
|
206
|
+
}
|
|
207
|
+
const moduleSet = new Set(modules.map((m) => m.path))
|
|
208
|
+
|
|
209
|
+
const resolveInclude = (absFile: string, specifier: string, angle: boolean): string | undefined => {
|
|
210
|
+
const candidates: string[] = []
|
|
211
|
+
if (angle) {
|
|
212
|
+
// `<foo/bar.h>`: try workspace-root-relative first, then the
|
|
213
|
+
// including file's directory (some projects quote-import adjacent).
|
|
214
|
+
candidates.push(path.join(root, specifier.split("/").join(path.sep)))
|
|
215
|
+
candidates.push(path.join(path.dirname(absFile), specifier.split("/").join(path.sep)))
|
|
216
|
+
} else {
|
|
217
|
+
// `"foo/bar.h"`: file-dir-relative first, then root-relative.
|
|
218
|
+
candidates.push(path.join(path.dirname(absFile), specifier.split("/").join(path.sep)))
|
|
219
|
+
candidates.push(path.join(root, specifier.split("/").join(path.sep)))
|
|
220
|
+
}
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
try {
|
|
223
|
+
if (fs.statSync(candidate).isFile()) {
|
|
224
|
+
return toWorkspaceRel(root, path.resolve(candidate))
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// keep trying
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const module of cppModules) {
|
|
234
|
+
const abs = path.join(root, module.path)
|
|
235
|
+
let source: string
|
|
236
|
+
try {
|
|
237
|
+
source = fs.readFileSync(abs, "utf-8")
|
|
238
|
+
} catch {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
const seen = new Set<string>()
|
|
242
|
+
const externals = new Set<string>()
|
|
243
|
+
INCLUDE_RE.lastIndex = 0
|
|
244
|
+
let match: RegExpExecArray | null
|
|
245
|
+
while ((match = INCLUDE_RE.exec(source)) !== null) {
|
|
246
|
+
const specifier = match[1]!
|
|
247
|
+
const angle = source[match.index + match[0].indexOf("<")] === "<"
|
|
248
|
+
const rel = resolveInclude(abs, specifier, angle)
|
|
249
|
+
const key = rel ?? `unresolved:${specifier}`
|
|
250
|
+
if (seen.has(key)) {
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
seen.add(key)
|
|
254
|
+
if (rel === undefined || !moduleSet.has(rel)) {
|
|
255
|
+
externals.add(specifier)
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
edges.push({ from: module.path, to: rel, kind: "include", specifier })
|
|
259
|
+
}
|
|
260
|
+
if (externals.size > 0) {
|
|
261
|
+
externalDeps[module.path] = [...externals].sort()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { edges, externalDeps }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run scripts/codemap-python-extract.py with the file list on stdin and
|
|
269
|
+
* return its stdout. undefined when python3 is missing or the script fails
|
|
270
|
+
* (callers treat that as "no python edges", never as a crash).
|
|
271
|
+
*/
|
|
272
|
+
function runPythonExtractor(root: string, files: string[]): Promise<string | undefined> {
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
const child = spawn("python3", [pythonExtractorScript()], { cwd: root })
|
|
275
|
+
let stdout = ""
|
|
276
|
+
let stderr = ""
|
|
277
|
+
child.stdout.on("data", (d: Buffer) => {
|
|
278
|
+
stdout += d.toString("utf-8")
|
|
279
|
+
if (stdout.length > 64 * 1024 * 1024) {
|
|
280
|
+
child.kill()
|
|
281
|
+
resolve(undefined)
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
child.stderr.on("data", (d: Buffer) => {
|
|
285
|
+
stderr += d.toString("utf-8")
|
|
286
|
+
})
|
|
287
|
+
child.on("error", (err) => {
|
|
288
|
+
resolve(undefined)
|
|
289
|
+
void err
|
|
290
|
+
})
|
|
291
|
+
child.on("close", (code) => {
|
|
292
|
+
if (code === 0) {
|
|
293
|
+
resolve(stdout)
|
|
294
|
+
} else {
|
|
295
|
+
console.warn(`[codemap] python extractor exited ${code}: ${stderr.trim().slice(0, 400)}`)
|
|
296
|
+
resolve(undefined)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
child.stdin.on("error", () => {
|
|
300
|
+
// stdin closed early (child died) — nothing to do
|
|
301
|
+
})
|
|
302
|
+
child.stdin.end(JSON.stringify({ root, files }))
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extract every import/include edge for the workspace's modules. Edges are
|
|
308
|
+
* deduplicated and sorted for determinism.
|
|
309
|
+
*/
|
|
310
|
+
export async function extractEdges(
|
|
311
|
+
root: string,
|
|
312
|
+
modules: ModuleEntry[],
|
|
313
|
+
): Promise<{ edges: CodemapEdge[]; externalDeps: Record<string, string[]> }> {
|
|
314
|
+
const ts = extractTypeScriptEdges(root, modules)
|
|
315
|
+
const py = await extractPythonEdges(root, modules)
|
|
316
|
+
const cpp = extractCppEdges(root, modules)
|
|
317
|
+
|
|
318
|
+
const edges = [...ts.edges, ...py.edges, ...cpp.edges]
|
|
319
|
+
const byKey = new Map<string, CodemapEdge>()
|
|
320
|
+
for (const edge of edges) {
|
|
321
|
+
const key = `${edge.from}\u0000${edge.to}\u0000${edge.kind}\u0000${edge.specifier}`
|
|
322
|
+
if (!byKey.has(key)) {
|
|
323
|
+
byKey.set(key, edge)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const deduped = [...byKey.values()].sort((a, b) => (a.from === b.from ? a.to.localeCompare(b.to) : a.from.localeCompare(b.from)))
|
|
327
|
+
|
|
328
|
+
const externalDeps: Record<string, string[]> = {}
|
|
329
|
+
for (const dep of [ts.externalDeps, py.externalDeps, cpp.externalDeps]) {
|
|
330
|
+
for (const [file, specs] of Object.entries(dep)) {
|
|
331
|
+
const merged = new Set(externalDeps[file] ?? [])
|
|
332
|
+
for (const s of specs) {
|
|
333
|
+
merged.add(s)
|
|
334
|
+
}
|
|
335
|
+
externalDeps[file] = [...merged].sort()
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { edges: deduped, externalDeps }
|
|
339
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module inventory for the codemap: git-aware file discovery + a
|
|
3
|
+
* naming-convention-based size/role heuristic (entrypoint, test, config,
|
|
4
|
+
* vendor/generated — NO semantics, per issue #17's scope).
|
|
5
|
+
*
|
|
6
|
+
* Discovery reuses src/codesearch/files.ts's `walkSourceFiles` — the
|
|
7
|
+
* git-aware walker (git ls-files with .gitignore semantics, generated-dir
|
|
8
|
+
* exclusions, a hard file cap) — and narrows its result to the source
|
|
9
|
+
* languages the codemap actually draws a graph over. Reusing it means the
|
|
10
|
+
* inventory and the codebase-search index can never disagree about what is a
|
|
11
|
+
* project file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs"
|
|
15
|
+
import * as path from "node:path"
|
|
16
|
+
|
|
17
|
+
import { walkSourceFiles } from "../codesearch/files.js"
|
|
18
|
+
import type { ModuleEntry, ModuleLanguage, ModuleRole } from "./types.js"
|
|
19
|
+
|
|
20
|
+
/** Source/config extensions the codemap inventories + draws edges for. */
|
|
21
|
+
const CODEMAP_EXTENSIONS = new Set([
|
|
22
|
+
".ts",
|
|
23
|
+
".tsx",
|
|
24
|
+
".mts",
|
|
25
|
+
".cts",
|
|
26
|
+
".js",
|
|
27
|
+
".jsx",
|
|
28
|
+
".mjs",
|
|
29
|
+
".cjs",
|
|
30
|
+
".py",
|
|
31
|
+
".c",
|
|
32
|
+
".h",
|
|
33
|
+
".cpp",
|
|
34
|
+
".hpp",
|
|
35
|
+
".cc",
|
|
36
|
+
".hh",
|
|
37
|
+
".cxx",
|
|
38
|
+
".hxx",
|
|
39
|
+
// Config files get inventoried (and role-tagged) even though they carry
|
|
40
|
+
// no import edges — a codemap that omits package.json/tsconfig.json is a
|
|
41
|
+
// map of the code without the knobs that shape it.
|
|
42
|
+
".json",
|
|
43
|
+
".toml",
|
|
44
|
+
".yaml",
|
|
45
|
+
".yml",
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
const TS_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
|
|
49
|
+
const JS_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs"])
|
|
50
|
+
const PY_EXTENSIONS = new Set([".py"])
|
|
51
|
+
const C_EXTENSIONS = new Set([".c", ".h"])
|
|
52
|
+
const CPP_EXTENSIONS = new Set([".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx"])
|
|
53
|
+
|
|
54
|
+
/** Map an extension to the coarse language family. */
|
|
55
|
+
export function languageForExtension(ext: string): ModuleLanguage {
|
|
56
|
+
const e = ext.toLowerCase()
|
|
57
|
+
if (TS_EXTENSIONS.has(e)) {
|
|
58
|
+
return "typescript"
|
|
59
|
+
}
|
|
60
|
+
if (JS_EXTENSIONS.has(e)) {
|
|
61
|
+
return "javascript"
|
|
62
|
+
}
|
|
63
|
+
if (PY_EXTENSIONS.has(e)) {
|
|
64
|
+
return "python"
|
|
65
|
+
}
|
|
66
|
+
if (CPP_EXTENSIONS.has(e)) {
|
|
67
|
+
return "cpp"
|
|
68
|
+
}
|
|
69
|
+
if (C_EXTENSIONS.has(e)) {
|
|
70
|
+
return "c"
|
|
71
|
+
}
|
|
72
|
+
return "other"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Test-file naming conventions across the three supported stacks. */
|
|
76
|
+
function isTestFile(rel: string, language: ModuleLanguage): boolean {
|
|
77
|
+
const base = path.basename(rel)
|
|
78
|
+
if (language === "python") {
|
|
79
|
+
return /^test_.*\.py$/.test(base) || /_test\.py$/.test(base)
|
|
80
|
+
}
|
|
81
|
+
if (language === "typescript" || language === "javascript") {
|
|
82
|
+
return /\.(test|spec)\.[cm]?[jt]sx?$/.test(base)
|
|
83
|
+
}
|
|
84
|
+
if (language === "cpp" || language === "c") {
|
|
85
|
+
return /(^|[_-])test[._-]/.test(base) || /_test\.(c|h|cpp|hpp|cc|hh|cxx|hxx)$/.test(base)
|
|
86
|
+
}
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Entrypoint naming conventions: main/cli/__main__ files. `__main__.py`
|
|
92
|
+
* (the `python -m` entry) and main.* count anywhere; cli/index count only
|
|
93
|
+
* when shallow (root or one dir deep, e.g. src/cli.ts) — a deep
|
|
94
|
+
* src/codesearch/cli.ts is a subcommand module, not a repo entrypoint.
|
|
95
|
+
* `__init__.py` is deliberately NOT an entrypoint: it is a package marker
|
|
96
|
+
* (a large Python repo alone can have ~190 of them; tagging them all as
|
|
97
|
+
* entrypoints drowns the real ones).
|
|
98
|
+
*/
|
|
99
|
+
function isEntrypointFile(rel: string): boolean {
|
|
100
|
+
const base = path.basename(rel)
|
|
101
|
+
if (/^main(\.[^.]*)?$/.test(base) || /^__main__\./.test(base) || rel.startsWith("bin/")) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
if (/^(cli|index)(\.[^.]*)?$/.test(base)) {
|
|
105
|
+
const depth = rel.split("/").length
|
|
106
|
+
return depth <= 2
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Config-file naming conventions: well-known manifests + config extensions. */
|
|
112
|
+
function isConfigFile(rel: string, language: ModuleLanguage): boolean {
|
|
113
|
+
const base = path.basename(rel).toLowerCase()
|
|
114
|
+
if (language !== "other") {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
return (
|
|
118
|
+
/\.(json|toml|ya?ml)$/.test(rel) ||
|
|
119
|
+
/\.config\.[a-z0-9]+$/.test(base) ||
|
|
120
|
+
[
|
|
121
|
+
"package.json",
|
|
122
|
+
"tsconfig.json",
|
|
123
|
+
"jsconfig.json",
|
|
124
|
+
"pyproject.toml",
|
|
125
|
+
"setup.cfg",
|
|
126
|
+
"requirements.txt",
|
|
127
|
+
"cargo.toml",
|
|
128
|
+
"go.mod",
|
|
129
|
+
"cmakelists.txt",
|
|
130
|
+
"makefile",
|
|
131
|
+
"dockerfile",
|
|
132
|
+
"compose.yaml",
|
|
133
|
+
"compose.yml",
|
|
134
|
+
".eslintrc",
|
|
135
|
+
".prettierrc",
|
|
136
|
+
".gitlab-ci.yml",
|
|
137
|
+
".github/workflows/",
|
|
138
|
+
].some((name) => rel.toLowerCase().includes(name))
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Vendor/generated directory-name heuristics (path-segment based). */
|
|
143
|
+
function isVendorOrGenerated(rel: string): { vendor: boolean; generated: boolean } {
|
|
144
|
+
const segments = rel.split("/")
|
|
145
|
+
let vendor = false
|
|
146
|
+
let generated = false
|
|
147
|
+
for (const segment of segments) {
|
|
148
|
+
if (["vendor", "third_party", "third-party", "node_modules", ".venv", "venv", "site-packages"].includes(segment)) {
|
|
149
|
+
vendor = true
|
|
150
|
+
}
|
|
151
|
+
if (["generated", "gen", "dist", "build", "out", "target"].includes(segment)) {
|
|
152
|
+
generated = true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Type declaration files are compiler outputs in spirit (they describe
|
|
156
|
+
// other code); they stay in the graph but are tagged generated.
|
|
157
|
+
if (/\.d\.[cm]?ts$/.test(rel)) {
|
|
158
|
+
generated = true
|
|
159
|
+
}
|
|
160
|
+
return { vendor, generated }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The naming-convention role for a module (source by default). */
|
|
164
|
+
export function classifyRole(rel: string, language: ModuleLanguage): ModuleRole {
|
|
165
|
+
const { vendor, generated } = isVendorOrGenerated(rel)
|
|
166
|
+
if (vendor) {
|
|
167
|
+
return "vendor"
|
|
168
|
+
}
|
|
169
|
+
if (generated) {
|
|
170
|
+
return "generated"
|
|
171
|
+
}
|
|
172
|
+
if (isTestFile(rel, language)) {
|
|
173
|
+
return "test"
|
|
174
|
+
}
|
|
175
|
+
if (isEntrypointFile(rel)) {
|
|
176
|
+
return "entrypoint"
|
|
177
|
+
}
|
|
178
|
+
if (isConfigFile(rel, language)) {
|
|
179
|
+
return "config"
|
|
180
|
+
}
|
|
181
|
+
return "source"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Read a file's byte size + line count (best-effort; 0s on read failure). */
|
|
185
|
+
export function statLines(abs: string): { sizeBytes: number; lineCount: number } {
|
|
186
|
+
try {
|
|
187
|
+
const st = fs.statSync(abs)
|
|
188
|
+
const raw = fs.readFileSync(abs, "utf-8")
|
|
189
|
+
let lines = 0
|
|
190
|
+
for (let i = 0; i < raw.length; i++) {
|
|
191
|
+
if (raw.charCodeAt(i) === 10) {
|
|
192
|
+
lines++
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { sizeBytes: st.size, lineCount: raw.length === 0 ? 0 : lines + 1 }
|
|
196
|
+
} catch {
|
|
197
|
+
return { sizeBytes: 0, lineCount: 0 }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Build the module inventory for a workspace: every supported source/config
|
|
203
|
+
* file with its role, language, size, line count and content hash. The hash
|
|
204
|
+
* is the fingerprint key the lock file uses for change detection.
|
|
205
|
+
*/
|
|
206
|
+
export async function inventoryModules(
|
|
207
|
+
workspaceRoot: string,
|
|
208
|
+
contentHash: (content: string) => string,
|
|
209
|
+
): Promise<ModuleEntry[]> {
|
|
210
|
+
const files = await walkSourceFiles(workspaceRoot)
|
|
211
|
+
const modules: ModuleEntry[] = []
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
const ext = path.extname(file.rel).toLowerCase()
|
|
214
|
+
if (!CODEMAP_EXTENSIONS.has(ext)) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
const language = languageForExtension(ext)
|
|
218
|
+
const { sizeBytes, lineCount } = statLines(file.abs)
|
|
219
|
+
let content: string
|
|
220
|
+
try {
|
|
221
|
+
content = fs.readFileSync(file.abs, "utf-8")
|
|
222
|
+
} catch {
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
modules.push({
|
|
226
|
+
path: file.rel,
|
|
227
|
+
language,
|
|
228
|
+
role: classifyRole(file.rel, language),
|
|
229
|
+
sizeBytes,
|
|
230
|
+
lineCount,
|
|
231
|
+
hash: contentHash(content),
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
modules.sort((a, b) => a.path.localeCompare(b.path))
|
|
235
|
+
return modules
|
|
236
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content hashing + lock comparison for the codemap's change detection.
|
|
3
|
+
*
|
|
4
|
+
* The lock file (codemap.lock) records a sha256 per module path. A rebuild
|
|
5
|
+
* recomputes the current hashes and compares them with the stored ones: an
|
|
6
|
+
* identical key-set AND identical hash per key means "nothing changed" and
|
|
7
|
+
* the build writes nothing. Any diff — a changed file, a new file, a deleted
|
|
8
|
+
* file — means a (full) regeneration. Full regen is deliberate for v1: the
|
|
9
|
+
* extraction is fast and cheap, so incremental per-module regeneration would
|
|
10
|
+
* add complexity with no measurable win (issue #17 explicitly allows it).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as crypto from "node:crypto"
|
|
14
|
+
|
|
15
|
+
import type { Codemap, CodemapLock } from "./types.js"
|
|
16
|
+
|
|
17
|
+
/** sha256 hex of a string (the same digest the codesearch index uses). */
|
|
18
|
+
export function contentHash(content: string): string {
|
|
19
|
+
return crypto.createHash("sha256").update(content, "utf-8").digest("hex")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Current fingerprint map for the given modules (path -> content hash). */
|
|
23
|
+
export function fingerprintMap(modules: ReadonlyArray<{ path: string; hash: string }>): Record<string, string> {
|
|
24
|
+
const out: Record<string, string> = {}
|
|
25
|
+
for (const m of modules) {
|
|
26
|
+
out[m.path] = m.hash
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True when the current modules exactly match the stored lock (no writes needed). */
|
|
32
|
+
export function lockMatches(lock: CodemapLock | undefined, modules: ReadonlyArray<{ path: string; hash: string }>): boolean {
|
|
33
|
+
if (lock === undefined) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
const current = fingerprintMap(modules)
|
|
37
|
+
const stored = lock.fingerprints
|
|
38
|
+
const currentKeys = Object.keys(current)
|
|
39
|
+
if (currentKeys.length !== Object.keys(stored).length) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
for (const key of currentKeys) {
|
|
43
|
+
if (stored[key] !== current[key]) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Deterministic content fingerprint for the whole codemap: sha256 over the
|
|
52
|
+
* sorted module paths+hashes and the sorted edges. Stable across
|
|
53
|
+
* regenerations of an unchanged repo.
|
|
54
|
+
*/
|
|
55
|
+
export function codemapFingerprint(codemap: Pick<Codemap, "project" | "modules" | "edges" | "externalDeps">): string {
|
|
56
|
+
const h = crypto.createHash("sha256")
|
|
57
|
+
const moduleLines = codemap.modules.map((m) => `${m.path}\t${m.hash}`).join("\n")
|
|
58
|
+
h.update(`modules:\n${moduleLines}\n`)
|
|
59
|
+
const edgeLines = codemap.edges
|
|
60
|
+
.map((e) => `${e.from}\t${e.to}\t${e.kind}\t${e.specifier}`)
|
|
61
|
+
.sort()
|
|
62
|
+
.join("\n")
|
|
63
|
+
h.update(`edges:\n${edgeLines}\n`)
|
|
64
|
+
return h.digest("hex")
|
|
65
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entrypoint-rooted end-to-end flows for the codemap (Phase 2, issue #18):
|
|
3
|
+
* for each entrypoint module, the sorted list of every module reachable from
|
|
4
|
+
* it along directed import+call edges.
|
|
5
|
+
*
|
|
6
|
+
* Phase 2 explicitly considered RANKING flows by "importance" (entrypoint
|
|
7
|
+
* fan-out, path length to a sink, …) and deliberately punted: every sketch
|
|
8
|
+
* either needed per-framework semantics (FastAPI routes, Celery tasks,
|
|
9
|
+
* pub/sub topics) that can't be derived without the project-specific pattern
|
|
10
|
+
* lists, or reduced to "sort by size of the reachable set" in a trench coat.
|
|
11
|
+
* The useful, honest half is surfacing the flows themselves — a human or a
|
|
12
|
+
* worker can filter them, while a wrong ranking is worse than no ranking
|
|
13
|
+
* (the original prompt's "Mark any relationship without source evidence as
|
|
14
|
+
* unknown. Do not guess."). Deterministic: same repo, same flows.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { CodemapEdge, ModuleEntry } from "./types.js"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compute the entrypoint-rooted flow map for a set of modules + edges.
|
|
21
|
+
*
|
|
22
|
+
* BFS along edge direction (imports, includes and calls all count — a call
|
|
23
|
+
* is as much a "flow" as an import). Result keys are the sorted entrypoint
|
|
24
|
+
* paths; values are the sorted reachable module paths, excluding the
|
|
25
|
+
* entrypoint itself. A pure function of modules + edges, so it is derived
|
|
26
|
+
* (not fingerprinted) by the build.
|
|
27
|
+
*/
|
|
28
|
+
export function computeEntrypointFlows(
|
|
29
|
+
modules: ReadonlyArray<Pick<ModuleEntry, "path" | "role">>,
|
|
30
|
+
edges: ReadonlyArray<Pick<CodemapEdge, "from" | "to">>,
|
|
31
|
+
): Record<string, string[]> {
|
|
32
|
+
const byPath = new Set(modules.map((m) => m.path))
|
|
33
|
+
const adjacency = new Map<string, string[]>()
|
|
34
|
+
for (const edge of edges) {
|
|
35
|
+
if (!byPath.has(edge.from) || !byPath.has(edge.to)) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const targets = adjacency.get(edge.from) ?? []
|
|
39
|
+
if (!targets.includes(edge.to)) {
|
|
40
|
+
targets.push(edge.to)
|
|
41
|
+
}
|
|
42
|
+
adjacency.set(edge.from, targets)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const flows: Record<string, string[]> = {}
|
|
46
|
+
for (const entrypoint of modules.filter((m) => m.role === "entrypoint").map((m) => m.path).sort()) {
|
|
47
|
+
const reachable = new Set<string>()
|
|
48
|
+
const queue = [...(adjacency.get(entrypoint) ?? [])]
|
|
49
|
+
while (queue.length > 0) {
|
|
50
|
+
const current = queue.shift()!
|
|
51
|
+
if (reachable.has(current)) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
reachable.add(current)
|
|
55
|
+
for (const next of adjacency.get(current) ?? []) {
|
|
56
|
+
queue.push(next)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
flows[entrypoint] = [...reachable].sort()
|
|
60
|
+
}
|
|
61
|
+
return flows
|
|
62
|
+
}
|