@praneeth_54/agentdoctor 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +141 -4
  2. package/README.md +329 -153
  3. package/dist/agent/approvals.d.ts +24 -0
  4. package/dist/agent/approvals.js +64 -0
  5. package/dist/agent/chat/index.d.ts +7 -0
  6. package/dist/agent/chat/index.js +5 -0
  7. package/dist/agent/chat/memory.d.ts +44 -0
  8. package/dist/agent/chat/memory.js +103 -0
  9. package/dist/agent/chat/project-summary.d.ts +16 -0
  10. package/dist/agent/chat/project-summary.js +70 -0
  11. package/dist/agent/chat/prompts.d.ts +6 -0
  12. package/dist/agent/chat/prompts.js +39 -0
  13. package/dist/agent/chat/response.d.ts +19 -0
  14. package/dist/agent/chat/response.js +109 -0
  15. package/dist/agent/chat/service.d.ts +42 -0
  16. package/dist/agent/chat/service.js +252 -0
  17. package/dist/agent/chat/types.d.ts +48 -0
  18. package/dist/agent/chat/types.js +1 -0
  19. package/dist/agent/context/retrieve.d.ts +21 -0
  20. package/dist/agent/context/retrieve.js +117 -0
  21. package/dist/agent/context/truth.d.ts +6 -0
  22. package/dist/agent/context/truth.js +19 -0
  23. package/dist/agent/context/types.d.ts +23 -0
  24. package/dist/agent/context/types.js +4 -0
  25. package/dist/agent/index.d.ts +26 -0
  26. package/dist/agent/index.js +14 -0
  27. package/dist/agent/loop.d.ts +51 -0
  28. package/dist/agent/loop.js +222 -0
  29. package/dist/agent/modes.d.ts +18 -0
  30. package/dist/agent/modes.js +102 -0
  31. package/dist/agent/plan.d.ts +30 -0
  32. package/dist/agent/plan.js +121 -0
  33. package/dist/agent/runtime.d.ts +67 -0
  34. package/dist/agent/runtime.js +180 -0
  35. package/dist/agent/state.d.ts +30 -0
  36. package/dist/agent/state.js +95 -0
  37. package/dist/agent/student.d.ts +56 -0
  38. package/dist/agent/student.js +230 -0
  39. package/dist/agent/tools/execute.d.ts +18 -0
  40. package/dist/agent/tools/execute.js +355 -0
  41. package/dist/agent/tools/index.d.ts +6 -0
  42. package/dist/agent/tools/index.js +5 -0
  43. package/dist/agent/tools/registry.d.ts +7 -0
  44. package/dist/agent/tools/registry.js +212 -0
  45. package/dist/agent/tools/run.d.ts +24 -0
  46. package/dist/agent/tools/run.js +44 -0
  47. package/dist/agent/tools/types.d.ts +32 -0
  48. package/dist/agent/tools/types.js +10 -0
  49. package/dist/agent/tools/write.d.ts +24 -0
  50. package/dist/agent/tools/write.js +121 -0
  51. package/dist/agent/verify.d.ts +29 -0
  52. package/dist/agent/verify.js +210 -0
  53. package/dist/ai/config.d.ts +22 -0
  54. package/dist/ai/config.js +68 -0
  55. package/dist/ai/index.d.ts +17 -0
  56. package/dist/ai/index.js +52 -0
  57. package/dist/ai/providers/mock.d.ts +20 -0
  58. package/dist/ai/providers/mock.js +84 -0
  59. package/dist/ai/providers/none.d.ts +6 -0
  60. package/dist/ai/providers/none.js +23 -0
  61. package/dist/ai/providers/openai-compatible.d.ts +21 -0
  62. package/dist/ai/providers/openai-compatible.js +151 -0
  63. package/dist/ai/redact.d.ts +8 -0
  64. package/dist/ai/redact.js +21 -0
  65. package/dist/ai/types.d.ts +71 -0
  66. package/dist/ai/types.js +6 -0
  67. package/dist/architecture/contract.d.ts +64 -0
  68. package/dist/architecture/contract.js +373 -0
  69. package/dist/assurance/change.d.ts +211 -0
  70. package/dist/assurance/change.js +624 -0
  71. package/dist/assurance/proof.d.ts +136 -0
  72. package/dist/assurance/proof.js +338 -0
  73. package/dist/auth/index.d.ts +81 -0
  74. package/dist/auth/index.js +130 -0
  75. package/dist/auth/rbac.d.ts +9 -0
  76. package/dist/auth/rbac.js +26 -0
  77. package/dist/cli/commands/agent.d.ts +27 -0
  78. package/dist/cli/commands/agent.js +153 -0
  79. package/dist/cli/commands/architecture.d.ts +6 -0
  80. package/dist/cli/commands/architecture.js +109 -0
  81. package/dist/cli/commands/assurance.d.ts +67 -0
  82. package/dist/cli/commands/assurance.js +210 -0
  83. package/dist/cli/commands/chat.d.ts +14 -0
  84. package/dist/cli/commands/chat.js +155 -0
  85. package/dist/cli/commands/complete.js +2 -2
  86. package/dist/cli/commands/learn.d.ts +12 -0
  87. package/dist/cli/commands/learn.js +107 -0
  88. package/dist/cli/commands/platform.d.ts +2 -0
  89. package/dist/cli/commands/platform.js +5 -1
  90. package/dist/cli/commands/policy-graph-run.d.ts +40 -0
  91. package/dist/cli/commands/policy-graph-run.js +200 -0
  92. package/dist/cli/commands/workspace.d.ts +9 -0
  93. package/dist/cli/commands/workspace.js +82 -0
  94. package/dist/cli/program.js +627 -17
  95. package/dist/constants.d.ts +1 -1
  96. package/dist/constants.js +1 -1
  97. package/dist/contracts/index.d.ts +1 -1
  98. package/dist/contracts/index.js +2 -2
  99. package/dist/core/brain-product/init.js +5 -2
  100. package/dist/core/secrets/scan.d.ts +7 -1
  101. package/dist/core/secrets/scan.js +9 -9
  102. package/dist/coverage/cobertura.d.ts +6 -0
  103. package/dist/coverage/cobertura.js +52 -0
  104. package/dist/coverage/istanbul.d.ts +7 -0
  105. package/dist/coverage/istanbul.js +134 -0
  106. package/dist/coverage/lcov.d.ts +7 -0
  107. package/dist/coverage/lcov.js +59 -0
  108. package/dist/coverage/load.d.ts +9 -0
  109. package/dist/coverage/load.js +39 -0
  110. package/dist/coverage/types.d.ts +23 -0
  111. package/dist/coverage/types.js +5 -0
  112. package/dist/dashboard/server.d.ts +7 -1
  113. package/dist/dashboard/server.js +110 -6
  114. package/dist/enforcement/runner.d.ts +52 -3
  115. package/dist/enforcement/runner.js +467 -16
  116. package/dist/index.d.ts +36 -2
  117. package/dist/index.js +23 -2
  118. package/dist/intelligence/graph/build.d.ts +2 -0
  119. package/dist/intelligence/graph/build.js +49 -5
  120. package/dist/intelligence/graph/incremental.d.ts +68 -0
  121. package/dist/intelligence/graph/incremental.js +234 -0
  122. package/dist/intelligence/resolve/imports.d.ts +36 -0
  123. package/dist/intelligence/resolve/imports.js +245 -0
  124. package/dist/languages/go.d.ts +9 -0
  125. package/dist/languages/go.js +50 -0
  126. package/dist/languages/index.d.ts +11 -0
  127. package/dist/languages/index.js +76 -0
  128. package/dist/languages/php.d.ts +8 -0
  129. package/dist/languages/php.js +202 -0
  130. package/dist/languages/python.d.ts +11 -0
  131. package/dist/languages/python.js +180 -0
  132. package/dist/languages/types.d.ts +42 -0
  133. package/dist/languages/types.js +26 -0
  134. package/dist/languages/typescript.d.ts +3 -0
  135. package/dist/languages/typescript.js +95 -0
  136. package/dist/mcp/agent/registry.d.ts +13 -0
  137. package/dist/mcp/agent/registry.js +234 -0
  138. package/dist/mcp/agentdoctor/server.js +8 -1
  139. package/dist/mcp/intelligence/handlers.d.ts +5 -0
  140. package/dist/mcp/intelligence/handlers.js +229 -40
  141. package/dist/mcp/intelligence/path-safety.d.ts +2 -4
  142. package/dist/mcp/intelligence/path-safety.js +25 -44
  143. package/dist/mcp/intelligence/registry.d.ts +1 -1
  144. package/dist/mcp/intelligence/registry.js +69 -1
  145. package/dist/platform/firewall/evaluate.d.ts +6 -3
  146. package/dist/platform/firewall/evaluate.js +66 -15
  147. package/dist/platform/graph/build.js +19 -23
  148. package/dist/platform/health/analyze.js +23 -17
  149. package/dist/platform/index.js +2 -0
  150. package/dist/platform/test-impact/analyze.d.ts +21 -3
  151. package/dist/platform/test-impact/analyze.js +214 -28
  152. package/dist/platform/tokens/plan.js +39 -20
  153. package/dist/platform/types.d.ts +9 -0
  154. package/dist/policy/compose.d.ts +34 -0
  155. package/dist/policy/compose.js +118 -0
  156. package/dist/policy/packs.js +33 -1
  157. package/dist/security/paths.d.ts +21 -0
  158. package/dist/security/paths.js +133 -0
  159. package/dist/storage/postgres.d.ts +26 -0
  160. package/dist/storage/postgres.js +90 -0
  161. package/dist/storage/provider.d.ts +2 -7
  162. package/dist/storage/provider.js +22 -16
  163. package/dist/storage/sqlite.d.ts +33 -0
  164. package/dist/storage/sqlite.js +77 -0
  165. package/dist/team/auth.js +1 -1
  166. package/dist/workspace/index.d.ts +69 -0
  167. package/dist/workspace/index.js +220 -0
  168. package/package.json +9 -3
@@ -5,10 +5,18 @@ import ts from "typescript";
5
5
  import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
6
6
  import { buildRepositoryGraph } from "../../platform/graph/build.js";
7
7
  import { CONTRACTS_VERSION } from "../../contracts/index.js";
8
- function nodeId(kind, key) {
8
+ import { loadTsconfigPaths, resolveImportSpecifier, } from "../resolve/imports.js";
9
+ function evidenceForConfidence(confidence) {
10
+ if (confidence === "EXACT")
11
+ return "verified";
12
+ if (confidence === "RESOLVED" || confidence === "INFERRED")
13
+ return "inferred";
14
+ return "unknown";
15
+ }
16
+ export function nodeId(kind, key) {
9
17
  return `${kind}:${createHash("sha1").update(key).digest("hex").slice(0, 12)}`;
10
18
  }
11
- async function listTsFiles(root, limit = 400) {
19
+ export async function listTsFiles(root, limit = 400) {
12
20
  const out = [];
13
21
  const skip = new Set(["node_modules", ".git", "dist", "coverage", ".agentdoctor", "vendor"]);
14
22
  async function walk(dir) {
@@ -78,6 +86,10 @@ export async function buildIntelligenceGraph(options) {
78
86
  const nodes = [];
79
87
  const edges = [];
80
88
  const seen = new Set();
89
+ // TypeScript uses `/` on Windows; path.join uses `\`. Normalize before matching.
90
+ const normalizeFsPath = (p) => path.normalize(p).toLowerCase();
91
+ const fileSet = new Set(files.map(normalizeFsPath));
92
+ const tsconfigPaths = loadTsconfigPaths(root);
81
93
  const pushNode = (n) => {
82
94
  if (seen.has(n.id))
83
95
  return;
@@ -87,7 +99,7 @@ export async function buildIntelligenceGraph(options) {
87
99
  for (const sf of program.getSourceFiles()) {
88
100
  if (sf.isDeclarationFile)
89
101
  continue;
90
- if (!files.includes(sf.fileName))
102
+ if (!fileSet.has(normalizeFsPath(sf.fileName)))
91
103
  continue;
92
104
  const rel = toPosixRelative(root, sf.fileName);
93
105
  const fileId = nodeId("file", rel);
@@ -143,21 +155,52 @@ export async function buildIntelligenceGraph(options) {
143
155
  node.moduleSpecifier &&
144
156
  ts.isStringLiteral(node.moduleSpecifier)) {
145
157
  const spec = node.moduleSpecifier.text;
158
+ const resolution = resolveImportSpecifier({
159
+ root,
160
+ fromFile: rel,
161
+ specifier: spec,
162
+ tsconfig: tsconfigPaths,
163
+ });
164
+ // Specifier dependency node (always — records the literal import).
146
165
  const toId = nodeId("dependency", `${rel}->${spec}`);
147
166
  pushNode({
148
167
  id: toId,
149
168
  kind: "dependency",
150
169
  label: spec,
151
170
  path: rel,
152
- meta: { specifier: spec, parser: "typescript-ast" },
171
+ meta: {
172
+ specifier: spec,
173
+ parser: "typescript-ast",
174
+ confidence: resolution.confidence,
175
+ ...(resolution.resolvedPath ? { resolvedPath: resolution.resolvedPath } : {}),
176
+ ...(resolution.via ? { via: resolution.via } : {}),
177
+ },
153
178
  });
154
179
  edges.push({
155
180
  id: nodeId("edge", `${fileId}->${toId}`),
156
181
  from: fileId,
157
182
  to: toId,
158
183
  kind: "imports",
159
- evidence: spec.startsWith(".") ? "verified" : "inferred",
184
+ evidence: evidenceForConfidence(resolution.confidence),
160
185
  });
186
+ // Concrete file→file edge only when resolved — never invent UNRESOLVED targets.
187
+ if (resolution.resolvedPath && resolution.confidence !== "UNRESOLVED") {
188
+ const targetFileId = nodeId("file", resolution.resolvedPath);
189
+ pushNode({
190
+ id: targetFileId,
191
+ kind: "file",
192
+ label: path.basename(resolution.resolvedPath),
193
+ path: resolution.resolvedPath,
194
+ meta: { importConfidence: resolution.confidence },
195
+ });
196
+ edges.push({
197
+ id: nodeId("edge", `${fileId}->file:${resolution.resolvedPath}`),
198
+ from: fileId,
199
+ to: targetFileId,
200
+ kind: "imports",
201
+ evidence: evidenceForConfidence(resolution.confidence),
202
+ });
203
+ }
161
204
  }
162
205
  if (ts.isCallExpression(node) &&
163
206
  ts.isIdentifier(node.expression) &&
@@ -195,6 +238,7 @@ export async function buildIntelligenceGraph(options) {
195
238
  limitations: [
196
239
  "TypeScript AST builder uses the TypeScript compiler API for .ts/.tsx only",
197
240
  "Cross-file call resolution is identifier-based (not full type-checker binding)",
241
+ "Import edges use EXACT|RESOLVED|INFERRED confidence; UNRESOLVED never invents file targets",
198
242
  "Non-TypeScript languages fall back to regex graph when requested via auto+empty TS set",
199
243
  `analysisVersion=${CONTRACTS_VERSION}`,
200
244
  ],
@@ -0,0 +1,68 @@
1
+ import type { GraphEdge, GraphNode } from "../../platform/types.js";
2
+ import { type GraphBuilderMode } from "./build.js";
3
+ export declare const GRAPH_INDEX_SCHEMA = "1.0.0";
4
+ export interface GraphSnapshot {
5
+ schemaVersion: string;
6
+ agentDoctorVersion: string;
7
+ root: string;
8
+ generatedAt: string;
9
+ builder: GraphBuilderMode | string;
10
+ astFilesParsed: number;
11
+ fileHashes: Record<string, string>;
12
+ nodes: GraphNode[];
13
+ edges: GraphEdge[];
14
+ limitations: string[];
15
+ }
16
+ export interface GraphStatus {
17
+ present: boolean;
18
+ path: string;
19
+ schemaVersion?: string;
20
+ builder?: string;
21
+ nodeCount: number;
22
+ edgeCount: number;
23
+ fileCount: number;
24
+ generatedAt?: string;
25
+ staleFiles: string[];
26
+ missingFiles: string[];
27
+ corrupt?: boolean;
28
+ error?: string;
29
+ }
30
+ /**
31
+ * Full graph build and persist to `.agentdoctor/graph/index.json`.
32
+ */
33
+ export declare function graphBuild(options: {
34
+ root: string;
35
+ mode?: GraphBuilderMode;
36
+ }): Promise<{
37
+ snapshot: GraphSnapshot;
38
+ path: string;
39
+ rebuilt: boolean;
40
+ }>;
41
+ /**
42
+ * Force full rebuild (same as build, clears prior assumptions).
43
+ */
44
+ export declare function graphRebuild(options: {
45
+ root: string;
46
+ mode?: GraphBuilderMode;
47
+ }): Promise<{
48
+ snapshot: GraphSnapshot;
49
+ path: string;
50
+ rebuilt: boolean;
51
+ }>;
52
+ /**
53
+ * Update graph from git-changed files or content-hash comparison.
54
+ * On corruption → rebuild. When nothing changed → reuse snapshot.
55
+ */
56
+ export declare function graphUpdate(options: {
57
+ root: string;
58
+ mode?: GraphBuilderMode;
59
+ }): Promise<{
60
+ snapshot: GraphSnapshot;
61
+ path: string;
62
+ rebuilt: boolean;
63
+ reason: string;
64
+ changedFiles: string[];
65
+ }>;
66
+ export declare function graphStatus(options: {
67
+ root: string;
68
+ }): Promise<GraphStatus>;
@@ -0,0 +1,234 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { PACKAGE_VERSION } from "../../constants.js";
6
+ import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
7
+ import { atomicWriteTextFile } from "../../utils/fs.js";
8
+ import { buildIntelligenceGraph, listTsFiles } from "./build.js";
9
+ export const GRAPH_INDEX_SCHEMA = "1.0.0";
10
+ function indexPath(root) {
11
+ return path.join(root, ".agentdoctor", "graph", "index.json");
12
+ }
13
+ function sha256FileContent(content) {
14
+ return createHash("sha256").update(content).digest("hex");
15
+ }
16
+ async function hashFiles(root, absFiles) {
17
+ const out = {};
18
+ for (const abs of absFiles) {
19
+ const rel = toPosixRelative(root, abs);
20
+ try {
21
+ const buf = await fs.readFile(abs);
22
+ out[rel] = sha256FileContent(buf);
23
+ }
24
+ catch {
25
+ // skip unreadable
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+ function gitChangedFiles(root) {
31
+ const run = (args) => {
32
+ const r = spawnSync("git", args, { cwd: root, encoding: "utf8" });
33
+ return r.status === 0 && typeof r.stdout === "string" ? r.stdout : null;
34
+ };
35
+ const staged = run(["diff", "--name-only", "--cached"]);
36
+ const unstaged = run(["diff", "--name-only"]);
37
+ const untracked = run(["ls-files", "--others", "--exclude-standard"]);
38
+ if (staged === null && unstaged === null && untracked === null)
39
+ return null;
40
+ const set = new Set();
41
+ for (const block of [staged, unstaged, untracked]) {
42
+ if (!block)
43
+ continue;
44
+ for (const line of block.split("\n")) {
45
+ const t = line.trim();
46
+ if (t)
47
+ set.add(t.split(path.sep).join("/"));
48
+ }
49
+ }
50
+ return [...set].sort();
51
+ }
52
+ function isValidSnapshot(raw) {
53
+ if (!raw || typeof raw !== "object")
54
+ return false;
55
+ const s = raw;
56
+ return (typeof s.schemaVersion === "string" &&
57
+ typeof s.root === "string" &&
58
+ typeof s.generatedAt === "string" &&
59
+ s.fileHashes !== null &&
60
+ typeof s.fileHashes === "object" &&
61
+ Array.isArray(s.nodes) &&
62
+ Array.isArray(s.edges));
63
+ }
64
+ async function loadSnapshot(root) {
65
+ const file = indexPath(root);
66
+ try {
67
+ const raw = await fs.readFile(file, "utf8");
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(raw);
71
+ }
72
+ catch {
73
+ return { ok: false, corrupt: true, error: "index.json is not valid JSON" };
74
+ }
75
+ if (!isValidSnapshot(parsed)) {
76
+ return { ok: false, corrupt: true, error: "index.json failed schema checks" };
77
+ }
78
+ return { ok: true, snapshot: parsed };
79
+ }
80
+ catch (error) {
81
+ return {
82
+ ok: false,
83
+ corrupt: false,
84
+ error: error instanceof Error ? error.message : "index missing",
85
+ };
86
+ }
87
+ }
88
+ async function persistSnapshot(root, snapshot) {
89
+ const file = indexPath(root);
90
+ await fs.mkdir(path.dirname(file), { recursive: true });
91
+ await atomicWriteTextFile(file, `${JSON.stringify(snapshot, null, 2)}\n`);
92
+ return file;
93
+ }
94
+ async function buildAndPersist(root, mode = "auto") {
95
+ const graph = await buildIntelligenceGraph({ root, mode });
96
+ const files = await listTsFiles(root);
97
+ const fileHashes = await hashFiles(root, files);
98
+ const snapshot = {
99
+ schemaVersion: GRAPH_INDEX_SCHEMA,
100
+ agentDoctorVersion: PACKAGE_VERSION,
101
+ root,
102
+ generatedAt: graph.generatedAt,
103
+ builder: graph.builder,
104
+ astFilesParsed: graph.astFilesParsed,
105
+ fileHashes,
106
+ nodes: graph.nodes,
107
+ edges: graph.edges,
108
+ limitations: [
109
+ ...graph.limitations,
110
+ "Incremental index persists file hashes for change detection",
111
+ ],
112
+ };
113
+ const saved = await persistSnapshot(root, snapshot);
114
+ return { snapshot, path: saved };
115
+ }
116
+ /**
117
+ * Full graph build and persist to `.agentdoctor/graph/index.json`.
118
+ */
119
+ export async function graphBuild(options) {
120
+ const root = resolveRepoRoot(options.root);
121
+ const result = await buildAndPersist(root, options.mode ?? "auto");
122
+ return { ...result, rebuilt: true };
123
+ }
124
+ /**
125
+ * Force full rebuild (same as build, clears prior assumptions).
126
+ */
127
+ export async function graphRebuild(options) {
128
+ return graphBuild(options);
129
+ }
130
+ /**
131
+ * Update graph from git-changed files or content-hash comparison.
132
+ * On corruption → rebuild. When nothing changed → reuse snapshot.
133
+ */
134
+ export async function graphUpdate(options) {
135
+ const root = resolveRepoRoot(options.root);
136
+ const loaded = await loadSnapshot(root);
137
+ if (!loaded.ok) {
138
+ if (loaded.corrupt) {
139
+ const result = await buildAndPersist(root, options.mode ?? "auto");
140
+ return {
141
+ ...result,
142
+ rebuilt: true,
143
+ reason: `corrupt index rebuilt: ${loaded.error}`,
144
+ changedFiles: Object.keys(result.snapshot.fileHashes),
145
+ };
146
+ }
147
+ const result = await buildAndPersist(root, options.mode ?? "auto");
148
+ return {
149
+ ...result,
150
+ rebuilt: true,
151
+ reason: "no prior index; built fresh",
152
+ changedFiles: Object.keys(result.snapshot.fileHashes),
153
+ };
154
+ }
155
+ const files = await listTsFiles(root);
156
+ const currentHashes = await hashFiles(root, files);
157
+ const prev = loaded.snapshot.fileHashes;
158
+ const changedFromHash = [];
159
+ for (const [rel, hash] of Object.entries(currentHashes)) {
160
+ if (prev[rel] !== hash)
161
+ changedFromHash.push(rel);
162
+ }
163
+ for (const rel of Object.keys(prev)) {
164
+ if (!(rel in currentHashes))
165
+ changedFromHash.push(rel);
166
+ }
167
+ const gitFiles = gitChangedFiles(root);
168
+ const tsGitChanged = gitFiles?.filter((f) => /\.(ts|tsx|mts|cts)$/i.test(f) && !f.endsWith(".d.ts")) ?? [];
169
+ const changedSet = new Set([...changedFromHash, ...tsGitChanged]);
170
+ const changedFiles = [...changedSet].sort();
171
+ if (changedFiles.length === 0) {
172
+ return {
173
+ snapshot: loaded.snapshot,
174
+ path: indexPath(root),
175
+ rebuilt: false,
176
+ reason: "file hashes unchanged; snapshot reused",
177
+ changedFiles: [],
178
+ };
179
+ }
180
+ // Incremental: drop nodes/edges for changed paths, re-run full builder for correctness
181
+ // (TypeScript program needs whole set; hash gate already skipped no-op updates).
182
+ const result = await buildAndPersist(root, options.mode ?? "auto");
183
+ return {
184
+ ...result,
185
+ rebuilt: true,
186
+ reason: `updated ${changedFiles.length} changed file(s)`,
187
+ changedFiles,
188
+ };
189
+ }
190
+ export async function graphStatus(options) {
191
+ const root = resolveRepoRoot(options.root);
192
+ const file = indexPath(root);
193
+ const loaded = await loadSnapshot(root);
194
+ if (!loaded.ok) {
195
+ return {
196
+ present: false,
197
+ path: file,
198
+ nodeCount: 0,
199
+ edgeCount: 0,
200
+ fileCount: 0,
201
+ corrupt: loaded.corrupt,
202
+ error: loaded.error,
203
+ staleFiles: [],
204
+ missingFiles: [],
205
+ };
206
+ }
207
+ const files = await listTsFiles(root);
208
+ const currentHashes = await hashFiles(root, files);
209
+ const prev = loaded.snapshot.fileHashes;
210
+ const staleFiles = [];
211
+ const missingFiles = [];
212
+ for (const [rel, hash] of Object.entries(currentHashes)) {
213
+ if (!(rel in prev))
214
+ staleFiles.push(rel);
215
+ else if (prev[rel] !== hash)
216
+ staleFiles.push(rel);
217
+ }
218
+ for (const rel of Object.keys(prev)) {
219
+ if (!(rel in currentHashes))
220
+ missingFiles.push(rel);
221
+ }
222
+ return {
223
+ present: true,
224
+ path: file,
225
+ schemaVersion: loaded.snapshot.schemaVersion,
226
+ builder: String(loaded.snapshot.builder),
227
+ nodeCount: loaded.snapshot.nodes.length,
228
+ edgeCount: loaded.snapshot.edges.length,
229
+ fileCount: Object.keys(prev).length,
230
+ generatedAt: loaded.snapshot.generatedAt,
231
+ staleFiles: staleFiles.sort(),
232
+ missingFiles: missingFiles.sort(),
233
+ };
234
+ }
@@ -0,0 +1,36 @@
1
+ export type ImportConfidence = "EXACT" | "RESOLVED" | "INFERRED" | "UNRESOLVED";
2
+ export interface TsconfigPathsConfig {
3
+ baseUrl: string;
4
+ paths: Record<string, string[]>;
5
+ configPath: string;
6
+ }
7
+ export interface ImportResolutionResult {
8
+ specifier: string;
9
+ fromFile: string;
10
+ resolvedPath: string | null;
11
+ confidence: ImportConfidence;
12
+ via?: string;
13
+ }
14
+ /**
15
+ * Read tsconfig.json paths/baseUrl when present. Returns null when absent or unreadable.
16
+ * Does not invent mappings.
17
+ */
18
+ export declare function loadTsconfigPaths(root: string): TsconfigPathsConfig | null;
19
+ /**
20
+ * Resolve an import specifier relative to `fromFile` (repo-relative POSIX).
21
+ * Never invents a path for UNRESOLVED — `resolvedPath` is null.
22
+ */
23
+ export declare function resolveImportSpecifier(options: {
24
+ root: string;
25
+ fromFile: string;
26
+ specifier: string;
27
+ tsconfig?: TsconfigPathsConfig | null;
28
+ fileExists?: (absolutePath: string) => boolean;
29
+ }): ImportResolutionResult;
30
+ /**
31
+ * Resolve many import edges; filters to those with a concrete resolved path.
32
+ */
33
+ export declare function resolveImportEdges(root: string, imports: Array<{
34
+ fromFile: string;
35
+ specifier: string;
36
+ }>, tsconfig?: TsconfigPathsConfig | null): ImportResolutionResult[];
@@ -0,0 +1,245 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
4
+ function toPosix(p) {
5
+ return p.split(path.sep).join("/");
6
+ }
7
+ function fileExists(abs, existsFn) {
8
+ if (existsFn)
9
+ return existsFn(abs);
10
+ try {
11
+ return fs.statSync(abs).isFile();
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ function dirExists(abs, existsFn) {
18
+ if (existsFn) {
19
+ // Approximate: any index file under the dir.
20
+ return FILE_EXTENSIONS.some((ext) => existsFn(path.join(abs, `index${ext}`)));
21
+ }
22
+ try {
23
+ return fs.statSync(abs).isDirectory();
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ /**
30
+ * Try exact file, extension variants, then index/barrel.
31
+ * Returns confidence EXACT when the written path exists as-is,
32
+ * RESOLVED when found via extension or index.
33
+ */
34
+ function resolveFilesystemCandidate(root, absWithoutExt, existsFn) {
35
+ const tryFile = (abs, confidence, via) => {
36
+ if (fileExists(abs, existsFn)) {
37
+ return { abs, confidence, via };
38
+ }
39
+ return null;
40
+ };
41
+ const exact = tryFile(absWithoutExt, "EXACT", "exact");
42
+ if (exact)
43
+ return exact;
44
+ for (const ext of FILE_EXTENSIONS) {
45
+ if (absWithoutExt.endsWith(ext))
46
+ continue;
47
+ const hit = tryFile(`${absWithoutExt}${ext}`, "RESOLVED", `extension:${ext}`);
48
+ if (hit)
49
+ return hit;
50
+ }
51
+ if (dirExists(absWithoutExt, existsFn)) {
52
+ for (const ext of FILE_EXTENSIONS) {
53
+ const hit = tryFile(path.join(absWithoutExt, `index${ext}`), "RESOLVED", `index:index${ext}`);
54
+ if (hit)
55
+ return hit;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ function stripJsonComments(text) {
61
+ // Minimal strip for // and /* */ outside strings — enough for typical tsconfig.
62
+ let out = "";
63
+ let i = 0;
64
+ let inString = null;
65
+ while (i < text.length) {
66
+ const ch = text[i];
67
+ if (inString) {
68
+ out += ch;
69
+ if (ch === "\\" && i + 1 < text.length) {
70
+ out += text[i + 1];
71
+ i += 2;
72
+ continue;
73
+ }
74
+ if (ch === inString)
75
+ inString = null;
76
+ i += 1;
77
+ continue;
78
+ }
79
+ if (ch === '"' || ch === "'") {
80
+ inString = ch;
81
+ out += ch;
82
+ i += 1;
83
+ continue;
84
+ }
85
+ if (ch === "/" && text[i + 1] === "/") {
86
+ i += 2;
87
+ while (i < text.length && text[i] !== "\n")
88
+ i += 1;
89
+ continue;
90
+ }
91
+ if (ch === "/" && text[i + 1] === "*") {
92
+ i += 2;
93
+ while (i + 1 < text.length && !(text[i] === "*" && text[i + 1] === "/"))
94
+ i += 1;
95
+ i += 2;
96
+ continue;
97
+ }
98
+ // Trailing commas before } or ]
99
+ if (ch === "," && (text.slice(i + 1).match(/^\s*[}\]]/) ?? false)) {
100
+ i += 1;
101
+ continue;
102
+ }
103
+ out += ch;
104
+ i += 1;
105
+ }
106
+ return out;
107
+ }
108
+ /**
109
+ * Read tsconfig.json paths/baseUrl when present. Returns null when absent or unreadable.
110
+ * Does not invent mappings.
111
+ */
112
+ export function loadTsconfigPaths(root) {
113
+ const configPath = path.join(root, "tsconfig.json");
114
+ let raw;
115
+ try {
116
+ raw = fs.readFileSync(configPath, "utf8");
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ try {
122
+ const parsed = JSON.parse(stripJsonComments(raw));
123
+ const baseUrl = parsed.compilerOptions?.baseUrl ?? ".";
124
+ const paths = parsed.compilerOptions?.paths ?? {};
125
+ return { baseUrl, paths, configPath };
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ function matchTsconfigPath(specifier, config) {
132
+ const entries = Object.entries(config.paths);
133
+ // Prefer longest pattern key for specificity.
134
+ entries.sort((a, b) => b[0].length - a[0].length);
135
+ for (const [pattern, targets] of entries) {
136
+ if (!targets.length)
137
+ continue;
138
+ if (pattern.includes("*")) {
139
+ const [prefix, suffix] = pattern.split("*");
140
+ if (typeof prefix === "string" &&
141
+ typeof suffix === "string" &&
142
+ specifier.startsWith(prefix) &&
143
+ specifier.endsWith(suffix)) {
144
+ const star = specifier.slice(prefix.length, specifier.length - suffix.length);
145
+ const target = targets[0];
146
+ const mapped = target.includes("*") ? target.replace("*", star) : target;
147
+ return { mapped, pattern };
148
+ }
149
+ }
150
+ else if (specifier === pattern) {
151
+ return { mapped: targets[0], pattern };
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Resolve an import specifier relative to `fromFile` (repo-relative POSIX).
158
+ * Never invents a path for UNRESOLVED — `resolvedPath` is null.
159
+ */
160
+ export function resolveImportSpecifier(options) {
161
+ const { root, fromFile, specifier } = options;
162
+ const fromPosix = toPosix(fromFile);
163
+ const base = {
164
+ specifier,
165
+ fromFile: fromPosix,
166
+ };
167
+ if (!specifier || specifier.includes("\0")) {
168
+ return { ...base, resolvedPath: null, confidence: "UNRESOLVED" };
169
+ }
170
+ const existsFn = options.fileExists;
171
+ const toRel = (abs) => toPosix(path.relative(root, abs));
172
+ // Relative imports
173
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
174
+ const fromDir = path.dirname(path.join(root, fromPosix));
175
+ const abs = path.resolve(fromDir, specifier);
176
+ // Containment: refuse escaping the repo when resolving relatives.
177
+ const rel = path.relative(root, abs);
178
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
179
+ return { ...base, resolvedPath: null, confidence: "UNRESOLVED" };
180
+ }
181
+ const hit = resolveFilesystemCandidate(root, abs, existsFn);
182
+ if (!hit) {
183
+ return { ...base, resolvedPath: null, confidence: "UNRESOLVED" };
184
+ }
185
+ return {
186
+ ...base,
187
+ resolvedPath: toRel(hit.abs),
188
+ confidence: hit.confidence,
189
+ via: hit.via,
190
+ };
191
+ }
192
+ // tsconfig paths / baseUrl (non-relative)
193
+ const tsconfig = options.tsconfig === undefined ? loadTsconfigPaths(root) : options.tsconfig;
194
+ if (tsconfig) {
195
+ const matched = matchTsconfigPath(specifier, tsconfig);
196
+ if (matched) {
197
+ const abs = path.resolve(root, tsconfig.baseUrl, matched.mapped);
198
+ const rel = path.relative(root, abs);
199
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
200
+ return { ...base, resolvedPath: null, confidence: "UNRESOLVED" };
201
+ }
202
+ const hit = resolveFilesystemCandidate(root, abs, existsFn);
203
+ if (hit) {
204
+ return {
205
+ ...base,
206
+ resolvedPath: toRel(hit.abs),
207
+ confidence: "RESOLVED",
208
+ via: `tsconfig-paths:${matched.pattern}`,
209
+ };
210
+ }
211
+ // Pattern matched but file missing — do not invent.
212
+ return { ...base, resolvedPath: null, confidence: "UNRESOLVED", via: "tsconfig-paths-miss" };
213
+ }
214
+ // baseUrl bare specifier (e.g. "utils/foo" with baseUrl=src) — INFERRED only when found.
215
+ if (!specifier.startsWith(".") && !specifier.includes(":")) {
216
+ const abs = path.resolve(root, tsconfig.baseUrl, specifier);
217
+ const rel = path.relative(root, abs);
218
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
219
+ const hit = resolveFilesystemCandidate(root, abs, existsFn);
220
+ if (hit) {
221
+ return {
222
+ ...base,
223
+ resolvedPath: toRel(hit.abs),
224
+ confidence: "INFERRED",
225
+ via: "tsconfig-baseUrl",
226
+ };
227
+ }
228
+ }
229
+ }
230
+ }
231
+ // Package / URL / unknown — unresolved; never invent a repo edge.
232
+ return { ...base, resolvedPath: null, confidence: "UNRESOLVED" };
233
+ }
234
+ /**
235
+ * Resolve many import edges; filters to those with a concrete resolved path.
236
+ */
237
+ export function resolveImportEdges(root, imports, tsconfig) {
238
+ const cfg = tsconfig === undefined ? loadTsconfigPaths(root) : tsconfig;
239
+ return imports.map((item) => resolveImportSpecifier({
240
+ root,
241
+ fromFile: item.fromFile,
242
+ specifier: item.specifier,
243
+ tsconfig: cfg,
244
+ }));
245
+ }
@@ -0,0 +1,9 @@
1
+ import type { LanguageAdapter } from "./types.js";
2
+ /**
3
+ * Go toolchain bridge placeholder.
4
+ * Prefer real `go/ast` via `go run` when the Go toolchain is present; until an
5
+ * extractor is bundled, capability remains unsupported even if `go` exists.
6
+ * Today: always unsupported (no bundled extractor; avoids fake AST claims).
7
+ */
8
+ export declare function goAvailable(): boolean;
9
+ export declare const goAdapter: LanguageAdapter;