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.
Files changed (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. 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
+ }