@sagentlab/navarch-runtime 0.1.39 → 0.1.40

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/README.md CHANGED
@@ -716,3 +716,26 @@ not match, making it a clean, zero-touch way to keep this package fully
716
716
  isolated. Vite/Vitest's default esbuild transform filter also excludes
717
717
  `.cts` by default — `vitest.config.cts` overrides it (`esbuild.include`) so
718
718
  tests are actually type-stripped and run.
719
+
720
+ ## Inspect code dependencies
721
+
722
+ `navarch-runtime code-graph --query saveInvoice --mode impact --commit HEAD`
723
+ returns JSON with the indexed commit, checkout HEAD, dirty-worktree warning,
724
+ source paths and line numbers, and bounded dependency edges. Modes are `search`,
725
+ `callers`, `callees`, and `impact`; impact accepts `--depth 1` through `5`.
726
+ Use an exact node ID to disambiguate duplicate symbol names. A file query includes
727
+ its functions and file import dependencies. `--repo` defaults to the current
728
+ working directory. The CLI works without a Navarch login or network access.
729
+
730
+ This first implementation analyzes committed JavaScript and TypeScript with the
731
+ TypeScript compiler. It resolves static imports and named functions/methods,
732
+ including imported aliases. It does not execute repository code, follow symlinks,
733
+ load tsconfig plugins, or upload source. Root tsconfig path aliases are supported;
734
+ extended configs, dynamic dispatch, external dependencies and other languages
735
+ are not fully resolved. Warnings report omissions. No graph is an assurance that
736
+ a change has no other effects: inspect the actual diff and run relevant tests.
737
+
738
+ Each invocation rebuilds from Git blobs at the requested commit, avoiding stale
739
+ persistent indexes. Bounds are 2,000 files, 10 MB total source, 500 KB per file,
740
+ 100 returned nodes and 300 edges. A later persistent index can optimize large
741
+ repositories without changing the query format.
package/dist/cli.cjs CHANGED
@@ -307,6 +307,7 @@ Usage:
307
307
  navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini|opencode]
308
308
  navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini|opencode]
309
309
  navarch-runtime doctor [--config-dir <path>]
310
+ navarch-runtime code-graph --query <symbol-or-file> [--mode search|callers|callees|impact] [--commit HEAD] [--depth 1..5] [--repo <path>]
310
311
 
311
312
  Use a different --config-dir (or NAVARCH_CONFIG_DIR) for every agent instance.
312
313
  Other configuration is via NAVARCH_* environment variables; see runtime/README.md.
@@ -328,6 +329,11 @@ async function main(argv = process.argv.slice(2)) {
328
329
  case "supervise":
329
330
  await superviseCommand(flags);
330
331
  break;
332
+ case "code-graph": {
333
+ const { codeGraphCommand } = await import("./code-graph.cjs");
334
+ console.log(JSON.stringify(codeGraphCommand(flags), null, 2));
335
+ break;
336
+ }
331
337
  case "doctor":
332
338
  await doctorCommand(flags);
333
339
  break;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readCodeSnapshot = readCodeSnapshot;
7
+ exports.buildCodeGraph = buildCodeGraph;
8
+ exports.queryCodeGraph = queryCodeGraph;
9
+ exports.codeGraphCommand = codeGraphCommand;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const typescript_1 = __importDefault(require("typescript"));
13
+ const ROOT = "/navarch-repo/";
14
+ const MAX_BYTES = 10_000_000;
15
+ const MAX_FILES = 2_000;
16
+ /** Reads committed blobs only. Never follows worktree symlinks, runs project code, or loads plugins. */
17
+ function readCodeSnapshot(cwd, ref = "HEAD") {
18
+ const git = (args, input) => (0, node_child_process_1.execFileSync)("git", args, {
19
+ cwd, input, encoding: "utf8", maxBuffer: 16_000_000, timeout: 15_000,
20
+ });
21
+ const commit = git(["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]).trim();
22
+ const head = git(["rev-parse", "HEAD"]).trim();
23
+ const dirty = Boolean(git(["status", "--porcelain", "--untracked-files=no"]).trim());
24
+ const warnings = [];
25
+ if (dirty)
26
+ warnings.push("Tracked worktree edits are not indexed; this graph describes committed code.");
27
+ if (commit !== head)
28
+ warnings.push("Indexed commit differs from checkout HEAD.");
29
+ const files = [];
30
+ let bytes = 0;
31
+ for (const entry of git(["ls-tree", "--full-tree", "-rlz", commit]).split("\0")) {
32
+ const match = /^(100644|100755) blob ([a-f0-9]+) +([0-9]+)\t(.+)$/s.exec(entry);
33
+ if (!match)
34
+ continue;
35
+ const file = match[4];
36
+ if ((!/\.(?:[cm]?[jt]sx?)$/.test(file) && file !== "tsconfig.json") ||
37
+ /(^|\/)(node_modules|dist|build|vendor|\.next)\//.test(file))
38
+ continue;
39
+ const size = Number(match[3]);
40
+ if (/[\r\n]/.test(file) || size > 500_000 || files.length >= MAX_FILES || bytes + size > MAX_BYTES) {
41
+ warnings.push(`Skipped source: ${file}`);
42
+ continue;
43
+ }
44
+ files.push({ file, sha: match[2], size });
45
+ bytes += size;
46
+ }
47
+ const contents = {};
48
+ if (files.length) {
49
+ // Object IDs, rather than paths, form the batch protocol input.
50
+ const output = (0, node_child_process_1.execFileSync)("git", ["cat-file", "--batch"], {
51
+ cwd, input: files.map((f) => f.sha).join("\n") + "\n", maxBuffer: 16_000_000, timeout: 15_000,
52
+ });
53
+ let offset = 0;
54
+ for (const file of files) {
55
+ const end = output.indexOf(10, offset);
56
+ if (end < 0 || output.subarray(offset, end).toString() !== `${file.sha} blob ${file.size}`) {
57
+ throw new Error("Invalid git object batch");
58
+ }
59
+ offset = end + 1;
60
+ contents[file.file] = output.subarray(offset, offset + file.size).toString("utf8");
61
+ offset += file.size + 1;
62
+ }
63
+ }
64
+ return { commit, head, dirty, contents, warnings };
65
+ }
66
+ function buildCodeGraph(snapshot) {
67
+ const warnings = [...snapshot.warnings];
68
+ const sources = new Map(Object.entries(snapshot.contents).map(([file, text]) => [ROOT + file, text]));
69
+ const config = sources.get(ROOT + "tsconfig.json");
70
+ const raw = config ? typescript_1.default.parseConfigFileTextToJson("tsconfig.json", config) : { config: {} };
71
+ if (raw.error)
72
+ warnings.push("Could not parse tsconfig.json; using default module resolution.");
73
+ if (raw.config?.extends)
74
+ warnings.push("Inherited tsconfig options are not loaded; unresolved aliases may be omitted.");
75
+ const converted = typescript_1.default.convertCompilerOptionsFromJson(raw.config?.compilerOptions ?? {}, ROOT);
76
+ const options = { ...converted.options, allowJs: true, noLib: true, noEmit: true };
77
+ const canonical = (file) => node_path_1.default.posix.normalize(file);
78
+ const host = {
79
+ fileExists: (file) => sources.has(canonical(file)),
80
+ readFile: (file) => sources.get(canonical(file)),
81
+ getSourceFile: (file, languageVersion) => {
82
+ const text = sources.get(canonical(file));
83
+ return text === undefined ? undefined : typescript_1.default.createSourceFile(file, text, languageVersion, true);
84
+ },
85
+ getDefaultLibFileName: () => "", writeFile: () => { }, getCurrentDirectory: () => ROOT,
86
+ getDirectories: () => [], directoryExists: (dir) => [...sources.keys()].some((f) => f.startsWith(dir.replace(/\/$/, "") + "/")),
87
+ getCanonicalFileName: canonical, useCaseSensitiveFileNames: () => true, getNewLine: () => "\n",
88
+ };
89
+ const program = typescript_1.default.createProgram([...sources.keys()].filter((f) => f !== ROOT + "tsconfig.json"), options, host);
90
+ const checker = program.getTypeChecker();
91
+ const nodes = [];
92
+ const declarations = new Map();
93
+ const ownedFunctions = new Map();
94
+ const sourceFiles = program.getSourceFiles().filter((f) => sources.has(f.fileName));
95
+ for (const source of sourceFiles) {
96
+ const file = source.fileName.slice(ROOT.length);
97
+ const fileId = `file:${file}`;
98
+ nodes.push({ id: fileId, name: file, file, line: 1, kind: "file" });
99
+ declarations.set(source, fileId);
100
+ const collect = (node) => {
101
+ let name;
102
+ let body;
103
+ if (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isGetAccessorDeclaration(node) || typescript_1.default.isSetAccessorDeclaration(node)) {
104
+ name = node.name;
105
+ body = node.body;
106
+ }
107
+ else if (typescript_1.default.isVariableDeclaration(node) && node.initializer &&
108
+ (typescript_1.default.isArrowFunction(node.initializer) || typescript_1.default.isFunctionExpression(node.initializer))) {
109
+ name = node.name;
110
+ body = node.initializer;
111
+ }
112
+ if (name && body) {
113
+ const position = source.getLineAndCharacterOfPosition(name.getStart(source));
114
+ const line = position.line + 1;
115
+ const id = `${file}:${line}:${position.character + 1}:${name.getText(source)}`;
116
+ nodes.push({ id, name: name.getText(source), file, line, kind: "function" });
117
+ declarations.set(node, id);
118
+ ownedFunctions.set(body, id);
119
+ }
120
+ typescript_1.default.forEachChild(node, collect);
121
+ };
122
+ collect(source);
123
+ }
124
+ const edges = [];
125
+ let unresolved = 0;
126
+ const target = (node) => {
127
+ let symbol = checker.getSymbolAtLocation(node);
128
+ if (symbol && symbol.flags & typescript_1.default.SymbolFlags.Alias)
129
+ symbol = checker.getAliasedSymbol(symbol);
130
+ const targets = new Set(symbol?.declarations?.map((d) => declarations.get(d)).filter((id) => Boolean(id)));
131
+ return targets.size === 1 ? [...targets][0] : undefined;
132
+ };
133
+ for (const source of sourceFiles) {
134
+ const visit = (node, parent) => {
135
+ const owner = ownedFunctions.get(node) ?? parent;
136
+ if (typescript_1.default.isCallExpression(node)) {
137
+ const expression = typescript_1.default.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression;
138
+ const to = target(expression);
139
+ if (to)
140
+ edges.push({ from: owner, to, kind: "call" });
141
+ else
142
+ unresolved++;
143
+ }
144
+ if (typescript_1.default.isImportDeclaration(node) || (typescript_1.default.isExportDeclaration(node) && node.moduleSpecifier)) {
145
+ const to = node.moduleSpecifier ? target(node.moduleSpecifier) : undefined;
146
+ if (to)
147
+ edges.push({ from: declarations.get(source), to, kind: "import" });
148
+ }
149
+ typescript_1.default.forEachChild(node, (child) => visit(child, owner));
150
+ };
151
+ visit(source, declarations.get(source));
152
+ }
153
+ warnings.push(`Static JS/TS analysis only; ${unresolved} external or unresolved calls omitted. Dynamic dispatch and runtime dependencies may be missing.`);
154
+ return { commit: snapshot.commit, head: snapshot.head, dirty: snapshot.dirty, nodes,
155
+ edges: [...new Map(edges.map((e) => [`${e.from}\0${e.to}\0${e.kind}`, e])).values()], warnings };
156
+ }
157
+ function queryCodeGraph(graph, query, mode = "search", depth = 3) {
158
+ if (!query.trim() || query.length > 500)
159
+ throw new Error("Provide a non-empty --query of at most 500 characters");
160
+ if (!["search", "callers", "callees", "impact"].includes(mode))
161
+ throw new Error("--mode must be search, callers, callees, or impact");
162
+ if (!Number.isInteger(depth) || depth < 1 || depth > 5)
163
+ throw new Error("--depth must be 1 through 5");
164
+ const exact = graph.nodes.filter((n) => n.id === query || n.name === query || n.file === query);
165
+ const matches = exact.length ? exact : graph.nodes.filter((n) => n.name.toLowerCase().includes(query.toLowerCase()));
166
+ const selected = new Set(matches.slice(0, 100).map((n) => n.id));
167
+ const paths = [];
168
+ let truncated = matches.length > 100;
169
+ if (mode !== "search") {
170
+ for (let hop = 0; hop < (mode === "impact" ? depth : 1); hop++) {
171
+ const frontier = new Set(selected);
172
+ for (const edge of graph.edges) {
173
+ const from = mode === "callees" ? edge.from : edge.to;
174
+ const to = mode === "callees" ? edge.to : edge.from;
175
+ if (!frontier.has(from))
176
+ continue;
177
+ if (!selected.has(to) && selected.size >= 100) {
178
+ truncated = true;
179
+ continue;
180
+ }
181
+ if (paths.length >= 300) {
182
+ truncated = true;
183
+ break;
184
+ }
185
+ selected.add(to);
186
+ if (!paths.some((p) => p.from === edge.from && p.to === edge.to && p.kind === edge.kind))
187
+ paths.push(edge);
188
+ }
189
+ }
190
+ }
191
+ return { commit: graph.commit, checkout_head: graph.head, dirty: graph.dirty, mode,
192
+ nodes: graph.nodes.filter((n) => selected.has(n.id)), edges: paths,
193
+ truncated, warnings: graph.warnings };
194
+ }
195
+ function codeGraphCommand(flags) {
196
+ const graph = buildCodeGraph(readCodeSnapshot(flags.repo ?? process.cwd(), flags.commit ?? "HEAD"));
197
+ return queryCodeGraph(graph, flags.query ?? "", flags.mode ?? "search", Number(flags.depth ?? 3));
198
+ }
package/dist/prompt.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderPrompt = renderPrompt;
4
4
  exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
5
+ const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
5
6
  /**
6
7
  * Renders the four-layer context bundle (project-plan.md §3.8: steering docs,
7
8
  * task itself, task-type playbook, depends_on reports) into the single prompt
@@ -14,7 +15,7 @@ function renderPrompt(task, bundle) {
14
15
  const repositoryContext = bundle.repository
15
16
  ? `Repository: ${bundle.repository.full_name} (${bundle.repository.url})\nLocal checkout: this session's isolated git worktree (current working directory).`
16
17
  : null;
17
- return [repositoryContext, bundle.task_context.trim(), bundle.retry_context?.trim()]
18
+ return [repositoryContext, bundle.task_context.trim(), CODE_GRAPH_GUIDANCE, bundle.retry_context?.trim()]
18
19
  .filter((section) => Boolean(section))
19
20
  .join("\n\n") + "\n";
20
21
  }
@@ -42,6 +43,7 @@ function renderPrompt(task, bundle) {
42
43
  }
43
44
  sections.push("## Playbook");
44
45
  sections.push(bundle.playbook);
46
+ sections.push(CODE_GRAPH_GUIDANCE);
45
47
  return sections.join("\n\n");
46
48
  }
47
49
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through Claude Code, Codex, Gemini, or OpenCode.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
@@ -47,7 +47,9 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^20.14.0",
50
- "typescript": "^5.7.2",
51
50
  "vitest": "^4.1.10"
51
+ },
52
+ "dependencies": {
53
+ "typescript": "^5.7.2"
52
54
  }
53
55
  }