@praneeth_54/agentdoctor 1.1.1 → 2.0.1
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/CHANGELOG.md +164 -7
- package/README.md +263 -513
- package/dist/agents/aider/adapter.d.ts +1 -0
- package/dist/agents/aider/adapter.js +1 -0
- package/dist/agents/aider/detector.d.ts +14 -0
- package/dist/agents/aider/detector.js +120 -0
- package/dist/agents/copilot/adapter.d.ts +1 -0
- package/dist/agents/copilot/adapter.js +1 -0
- package/dist/agents/copilot/detector.d.ts +13 -0
- package/dist/agents/copilot/detector.js +113 -0
- package/dist/agents/gemini/adapter.d.ts +1 -0
- package/dist/agents/gemini/adapter.js +1 -0
- package/dist/agents/gemini/detector.d.ts +13 -0
- package/dist/agents/gemini/detector.js +151 -0
- package/dist/agents/registry.js +13 -1
- package/dist/agents/types.d.ts +1 -1
- package/dist/agents/windsurf/adapter.d.ts +1 -0
- package/dist/agents/windsurf/adapter.js +1 -0
- package/dist/agents/windsurf/detector.d.ts +14 -0
- package/dist/agents/windsurf/detector.js +119 -0
- package/dist/architecture/c4.d.ts +22 -0
- package/dist/architecture/c4.js +62 -0
- package/dist/architecture/contract.d.ts +64 -0
- package/dist/architecture/contract.js +373 -0
- package/dist/assurance/change.d.ts +211 -0
- package/dist/assurance/change.js +624 -0
- package/dist/assurance/proof.d.ts +136 -0
- package/dist/assurance/proof.js +338 -0
- package/dist/auth/index.d.ts +81 -0
- package/dist/auth/index.js +130 -0
- package/dist/auth/rbac.d.ts +9 -0
- package/dist/auth/rbac.js +26 -0
- package/dist/cli/commands/architecture.d.ts +6 -0
- package/dist/cli/commands/architecture.js +109 -0
- package/dist/cli/commands/assurance.d.ts +67 -0
- package/dist/cli/commands/assurance.js +210 -0
- package/dist/cli/commands/brain.d.ts +12 -0
- package/dist/cli/commands/brain.js +162 -0
- package/dist/cli/commands/complete.d.ts +23 -0
- package/dist/cli/commands/complete.js +232 -0
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -2
- package/dist/cli/commands/mcp.d.ts +8 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/platform.d.ts +17 -0
- package/dist/cli/commands/platform.js +195 -0
- package/dist/cli/commands/policy-graph-run.d.ts +40 -0
- package/dist/cli/commands/policy-graph-run.js +200 -0
- package/dist/cli/commands/v2.d.ts +55 -0
- package/dist/cli/commands/v2.js +278 -0
- package/dist/cli/commands/workspace.d.ts +9 -0
- package/dist/cli/commands/workspace.js +82 -0
- package/dist/cli/program.js +1328 -3
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +5 -1
- package/dist/contracts/adapters.d.ts +7 -0
- package/dist/contracts/adapters.js +60 -0
- package/dist/contracts/index.d.ts +162 -0
- package/dist/contracts/index.js +18 -0
- package/dist/core/baseline/store.d.ts +48 -0
- package/dist/core/baseline/store.js +163 -0
- package/dist/core/brain-cli/service.d.ts +26 -0
- package/dist/core/brain-cli/service.js +175 -0
- package/dist/core/brain-product/init.d.ts +45 -0
- package/dist/core/brain-product/init.js +235 -0
- package/dist/core/changes/analyze.d.ts +41 -0
- package/dist/core/changes/analyze.js +223 -0
- package/dist/core/context-health/analyze.d.ts +15 -0
- package/dist/core/context-health/analyze.js +234 -0
- package/dist/core/fix/apply.d.ts +21 -4
- package/dist/core/fix/apply.js +118 -18
- package/dist/core/fix/backup.d.ts +30 -0
- package/dist/core/fix/backup.js +193 -0
- package/dist/core/fix/plan.d.ts +6 -3
- package/dist/core/fix/plan.js +113 -9
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +36 -21
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/safe-target.d.ts +14 -0
- package/dist/core/fix/safe-target.js +81 -0
- package/dist/core/fix/types.d.ts +12 -1
- package/dist/core/fix/types.js +2 -0
- package/dist/core/fix/writers/claude-settings.js +3 -1
- package/dist/core/fix/writers/codex-config.d.ts +3 -0
- package/dist/core/fix/writers/codex-config.js +24 -3
- package/dist/core/fix/writers/cursorignore.js +3 -1
- package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
- package/dist/core/fix/writers/simple-ignore.js +68 -0
- package/dist/core/monorepo/detect.d.ts +13 -0
- package/dist/core/monorepo/detect.js +121 -0
- package/dist/core/rules/build-context.js +22 -3
- package/dist/core/rules/context/generated-directory.js +11 -1
- package/dist/core/rules/context/large-instruction-file.js +6 -0
- package/dist/core/rules/context/large-log-file.js +16 -1
- package/dist/core/rules/ignore.d.ts +11 -1
- package/dist/core/rules/ignore.js +22 -1
- package/dist/core/rules/instructions/empty-instructions.js +7 -0
- package/dist/core/rules/security/env-file-exposure.js +23 -2
- package/dist/core/rules/security/private-key-file.js +1 -1
- package/dist/core/scanner/scan.js +1 -1
- package/dist/core/schemas/validate.d.ts +6 -0
- package/dist/core/schemas/validate.js +62 -0
- package/dist/core/scoring/compute-scores.d.ts +1 -1
- package/dist/core/scoring/compute-scores.js +5 -1
- package/dist/core/scoring/placeholder.d.ts +1 -1
- package/dist/core/scoring/placeholder.js +5 -1
- package/dist/core/secrets/scan.d.ts +29 -0
- package/dist/core/secrets/scan.js +172 -0
- package/dist/coverage/cobertura.d.ts +6 -0
- package/dist/coverage/cobertura.js +52 -0
- package/dist/coverage/istanbul.d.ts +7 -0
- package/dist/coverage/istanbul.js +134 -0
- package/dist/coverage/lcov.d.ts +7 -0
- package/dist/coverage/lcov.js +59 -0
- package/dist/coverage/load.d.ts +9 -0
- package/dist/coverage/load.js +39 -0
- package/dist/coverage/types.d.ts +23 -0
- package/dist/coverage/types.js +5 -0
- package/dist/dashboard/server.d.ts +24 -0
- package/dist/dashboard/server.js +402 -0
- package/dist/enforcement/runner.d.ts +67 -0
- package/dist/enforcement/runner.js +460 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +25 -0
- package/dist/integrations/github/pr-analyze.d.ts +28 -0
- package/dist/integrations/github/pr-analyze.js +98 -0
- package/dist/integrations/local-ai/provider.d.ts +45 -0
- package/dist/integrations/local-ai/provider.js +104 -0
- package/dist/intelligence/git/analyze.d.ts +34 -0
- package/dist/intelligence/git/analyze.js +136 -0
- package/dist/intelligence/graph/build.d.ts +15 -0
- package/dist/intelligence/graph/build.js +259 -0
- package/dist/intelligence/graph/incremental.d.ts +68 -0
- package/dist/intelligence/graph/incremental.js +234 -0
- package/dist/intelligence/resolve/imports.d.ts +36 -0
- package/dist/intelligence/resolve/imports.js +245 -0
- package/dist/knowledge/store.d.ts +25 -0
- package/dist/knowledge/store.js +91 -0
- package/dist/languages/go.d.ts +9 -0
- package/dist/languages/go.js +45 -0
- package/dist/languages/index.d.ts +11 -0
- package/dist/languages/index.js +76 -0
- package/dist/languages/php.d.ts +7 -0
- package/dist/languages/php.js +193 -0
- package/dist/languages/python.d.ts +7 -0
- package/dist/languages/python.js +150 -0
- package/dist/languages/types.d.ts +42 -0
- package/dist/languages/types.js +26 -0
- package/dist/languages/typescript.d.ts +3 -0
- package/dist/languages/typescript.js +95 -0
- package/dist/mcp/agentdoctor/server.d.ts +16 -0
- package/dist/mcp/agentdoctor/server.js +67 -0
- package/dist/mcp/brain/session.js +7 -7
- package/dist/mcp/intelligence/handlers.d.ts +16 -0
- package/dist/mcp/intelligence/handlers.js +364 -0
- package/dist/mcp/intelligence/path-safety.d.ts +9 -0
- package/dist/mcp/intelligence/path-safety.js +63 -0
- package/dist/mcp/intelligence/registry.d.ts +8 -0
- package/dist/mcp/intelligence/registry.js +228 -0
- package/dist/ops/health.d.ts +18 -0
- package/dist/ops/health.js +51 -0
- package/dist/platform/ai-quality/analyze.d.ts +9 -0
- package/dist/platform/ai-quality/analyze.js +65 -0
- package/dist/platform/architecture/drift.d.ts +17 -0
- package/dist/platform/architecture/drift.js +74 -0
- package/dist/platform/auth/local.d.ts +15 -0
- package/dist/platform/auth/local.js +58 -0
- package/dist/platform/context-security/analyze.d.ts +6 -0
- package/dist/platform/context-security/analyze.js +97 -0
- package/dist/platform/firewall/evaluate.d.ts +57 -0
- package/dist/platform/firewall/evaluate.js +359 -0
- package/dist/platform/graph/build.d.ts +6 -0
- package/dist/platform/graph/build.js +189 -0
- package/dist/platform/health/analyze.d.ts +7 -0
- package/dist/platform/health/analyze.js +192 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/index.js +89 -0
- package/dist/platform/knowledge/analyze.d.ts +20 -0
- package/dist/platform/knowledge/analyze.js +96 -0
- package/dist/platform/provenance/build.d.ts +44 -0
- package/dist/platform/provenance/build.js +65 -0
- package/dist/platform/readiness/scorecard.d.ts +5 -0
- package/dist/platform/readiness/scorecard.js +98 -0
- package/dist/platform/refactor/impact.d.ts +20 -0
- package/dist/platform/refactor/impact.js +60 -0
- package/dist/platform/reports/export.d.ts +12 -0
- package/dist/platform/reports/export.js +92 -0
- package/dist/platform/security/redact.d.ts +15 -0
- package/dist/platform/security/redact.js +87 -0
- package/dist/platform/sessions/store.d.ts +32 -0
- package/dist/platform/sessions/store.js +97 -0
- package/dist/platform/store.d.ts +6 -0
- package/dist/platform/store.js +57 -0
- package/dist/platform/test-impact/analyze.d.ts +50 -0
- package/dist/platform/test-impact/analyze.js +350 -0
- package/dist/platform/time-machine/compare.d.ts +17 -0
- package/dist/platform/time-machine/compare.js +62 -0
- package/dist/platform/tokens/plan.d.ts +28 -0
- package/dist/platform/tokens/plan.js +108 -0
- package/dist/platform/types.d.ts +120 -0
- package/dist/platform/types.js +5 -0
- package/dist/plugins/runtime.d.ts +36 -0
- package/dist/plugins/runtime.js +101 -0
- package/dist/plugins/sdk.d.ts +29 -0
- package/dist/plugins/sdk.js +114 -0
- package/dist/policy/compose.d.ts +34 -0
- package/dist/policy/compose.js +118 -0
- package/dist/policy/packs.d.ts +9 -0
- package/dist/policy/packs.js +91 -0
- package/dist/reporters/terminal/report.js +38 -5
- package/dist/security/paths.d.ts +21 -0
- package/dist/security/paths.js +105 -0
- package/dist/storage/postgres.d.ts +26 -0
- package/dist/storage/postgres.js +90 -0
- package/dist/storage/provider.d.ts +30 -0
- package/dist/storage/provider.js +109 -0
- package/dist/storage/sqlite.d.ts +33 -0
- package/dist/storage/sqlite.js +77 -0
- package/dist/team/auth.d.ts +35 -0
- package/dist/team/auth.js +70 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/workspace/index.d.ts +69 -0
- package/dist/workspace/index.js +220 -0
- package/package.json +15 -5
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional local AI integration.
|
|
3
|
+
* Core AgentDoctor never requires a model. Providers are opt-in and must redact
|
|
4
|
+
* sensitive content before any optional call. AI output must not bypass Safe Fix.
|
|
5
|
+
*/
|
|
6
|
+
export type LocalAiProviderId = "none" | "mock" | "ollama";
|
|
7
|
+
export interface LocalAiRequest {
|
|
8
|
+
prompt: string;
|
|
9
|
+
/** Already-redacted context snippets only */
|
|
10
|
+
context: string[];
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface LocalAiResponse {
|
|
15
|
+
provider: LocalAiProviderId;
|
|
16
|
+
text: string;
|
|
17
|
+
/** Always true for AI-generated content so callers can label output */
|
|
18
|
+
aiGenerated: true;
|
|
19
|
+
truncated: boolean;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LocalAiProvider {
|
|
23
|
+
id: LocalAiProviderId;
|
|
24
|
+
generate(request: LocalAiRequest): Promise<LocalAiResponse>;
|
|
25
|
+
}
|
|
26
|
+
export declare function redactForModel(text: string): string;
|
|
27
|
+
export declare class NoneAiProvider implements LocalAiProvider {
|
|
28
|
+
id: LocalAiProviderId;
|
|
29
|
+
generate(_request: LocalAiRequest): Promise<LocalAiResponse>;
|
|
30
|
+
}
|
|
31
|
+
export declare class MockAiProvider implements LocalAiProvider {
|
|
32
|
+
id: LocalAiProviderId;
|
|
33
|
+
generate(request: LocalAiRequest): Promise<LocalAiResponse>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Ollama provider stub: documents integration without requiring ollama installed.
|
|
37
|
+
* Calls fail closed with a clear error when the daemon is unavailable.
|
|
38
|
+
*/
|
|
39
|
+
export declare class OllamaAiProvider implements LocalAiProvider {
|
|
40
|
+
private readonly endpoint;
|
|
41
|
+
id: LocalAiProviderId;
|
|
42
|
+
constructor(endpoint?: string);
|
|
43
|
+
generate(request: LocalAiRequest): Promise<LocalAiResponse>;
|
|
44
|
+
}
|
|
45
|
+
export declare function createLocalAiProvider(id: LocalAiProviderId): LocalAiProvider;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional local AI integration.
|
|
3
|
+
* Core AgentDoctor never requires a model. Providers are opt-in and must redact
|
|
4
|
+
* sensitive content before any optional call. AI output must not bypass Safe Fix.
|
|
5
|
+
*/
|
|
6
|
+
const REDACT = /\b(AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|-----BEGIN [^-]+PRIVATE KEY-----)/g;
|
|
7
|
+
export function redactForModel(text) {
|
|
8
|
+
return text.replace(REDACT, "[REDACTED]");
|
|
9
|
+
}
|
|
10
|
+
export class NoneAiProvider {
|
|
11
|
+
id = "none";
|
|
12
|
+
async generate(_request) {
|
|
13
|
+
return {
|
|
14
|
+
provider: "none",
|
|
15
|
+
text: "",
|
|
16
|
+
aiGenerated: true,
|
|
17
|
+
truncated: false,
|
|
18
|
+
error: "Local AI disabled (deterministic mode)",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class MockAiProvider {
|
|
23
|
+
id = "mock";
|
|
24
|
+
async generate(request) {
|
|
25
|
+
const joined = redactForModel([request.prompt, ...request.context].join("\n"));
|
|
26
|
+
const max = request.maxTokens ?? 256;
|
|
27
|
+
const text = `[AI-GENERATED mock] Summary of ${joined.length} redacted chars.`.slice(0, max);
|
|
28
|
+
return {
|
|
29
|
+
provider: "mock",
|
|
30
|
+
text,
|
|
31
|
+
aiGenerated: true,
|
|
32
|
+
truncated: joined.length > max,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Ollama provider stub: documents integration without requiring ollama installed.
|
|
38
|
+
* Calls fail closed with a clear error when the daemon is unavailable.
|
|
39
|
+
*/
|
|
40
|
+
export class OllamaAiProvider {
|
|
41
|
+
endpoint;
|
|
42
|
+
id = "ollama";
|
|
43
|
+
constructor(endpoint = "http://127.0.0.1:11434") {
|
|
44
|
+
this.endpoint = endpoint;
|
|
45
|
+
}
|
|
46
|
+
async generate(request) {
|
|
47
|
+
const timeoutMs = request.timeoutMs ?? 5_000;
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
const prompt = redactForModel(`${request.prompt}\n\n${request.context.map(redactForModel).join("\n")}`.slice(0, 8_000));
|
|
52
|
+
const res = await fetch(`${this.endpoint}/api/generate`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
model: "llama3.2",
|
|
57
|
+
prompt,
|
|
58
|
+
stream: false,
|
|
59
|
+
options: { num_predict: request.maxTokens ?? 256 },
|
|
60
|
+
}),
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
return {
|
|
65
|
+
provider: "ollama",
|
|
66
|
+
text: "",
|
|
67
|
+
aiGenerated: true,
|
|
68
|
+
truncated: false,
|
|
69
|
+
error: `ollama HTTP ${res.status}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const data = (await res.json());
|
|
73
|
+
return {
|
|
74
|
+
provider: "ollama",
|
|
75
|
+
text: `[AI-GENERATED] ${String(data.response ?? "").trim()}`,
|
|
76
|
+
aiGenerated: true,
|
|
77
|
+
truncated: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
provider: "ollama",
|
|
83
|
+
text: "",
|
|
84
|
+
aiGenerated: true,
|
|
85
|
+
truncated: false,
|
|
86
|
+
error: error instanceof Error ? error.message : String(error),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function createLocalAiProvider(id) {
|
|
95
|
+
switch (id) {
|
|
96
|
+
case "mock":
|
|
97
|
+
return new MockAiProvider();
|
|
98
|
+
case "ollama":
|
|
99
|
+
return new OllamaAiProvider();
|
|
100
|
+
case "none":
|
|
101
|
+
default:
|
|
102
|
+
return new NoneAiProvider();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface HotspotFile {
|
|
2
|
+
path: string;
|
|
3
|
+
commits: number;
|
|
4
|
+
authors: string[];
|
|
5
|
+
busFactor: number;
|
|
6
|
+
method: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GitIntelligenceReport {
|
|
9
|
+
root: string;
|
|
10
|
+
gitAvailable: boolean;
|
|
11
|
+
hotspots: HotspotFile[];
|
|
12
|
+
coChanges: Array<{
|
|
13
|
+
a: string;
|
|
14
|
+
b: string;
|
|
15
|
+
together: number;
|
|
16
|
+
method: string;
|
|
17
|
+
}>;
|
|
18
|
+
limitations: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Git engineering intelligence — methods disclosed on every metric.
|
|
22
|
+
*/
|
|
23
|
+
export declare function analyzeGitIntelligence(rootInput: string): Promise<GitIntelligenceReport>;
|
|
24
|
+
export declare function deadCodeCategory(options: {
|
|
25
|
+
exported: boolean;
|
|
26
|
+
referenced: boolean;
|
|
27
|
+
frameworkEntry: boolean;
|
|
28
|
+
dynamicHint: boolean;
|
|
29
|
+
}): {
|
|
30
|
+
category: "confirmed-unreachable" | "probably-unused" | "possibly-unused" | "public-api" | "framework-discovered" | "reflection-sensitive" | "dynamic-reference-risk" | "unknown";
|
|
31
|
+
confidence: number;
|
|
32
|
+
explanation: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function stableMetricId(kind: string, path: string): string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
4
|
+
function git(root, args) {
|
|
5
|
+
const r = spawnSync("git", args, {
|
|
6
|
+
cwd: root,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
9
|
+
});
|
|
10
|
+
return { ok: r.status === 0, out: typeof r.stdout === "string" ? r.stdout : "" };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Git engineering intelligence — methods disclosed on every metric.
|
|
14
|
+
*/
|
|
15
|
+
export async function analyzeGitIntelligence(rootInput) {
|
|
16
|
+
const root = resolveRepoRoot(rootInput);
|
|
17
|
+
const probe = git(root, ["rev-parse", "--is-inside-work-tree"]);
|
|
18
|
+
if (!probe.ok || probe.out.trim() !== "true") {
|
|
19
|
+
return {
|
|
20
|
+
root,
|
|
21
|
+
gitAvailable: false,
|
|
22
|
+
hotspots: [],
|
|
23
|
+
coChanges: [],
|
|
24
|
+
limitations: ["Requires a git repository"],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const log = git(root, ["log", "--name-only", "--pretty=format:===%an", "-n", "200"]);
|
|
28
|
+
const authorsByFile = new Map();
|
|
29
|
+
const commitsByFile = new Map();
|
|
30
|
+
const commitFiles = [];
|
|
31
|
+
let currentAuthor = "unknown";
|
|
32
|
+
let bucket = [];
|
|
33
|
+
for (const line of log.out.split(/\r?\n/)) {
|
|
34
|
+
if (line.startsWith("===")) {
|
|
35
|
+
if (bucket.length)
|
|
36
|
+
commitFiles.push(bucket);
|
|
37
|
+
bucket = [];
|
|
38
|
+
currentAuthor = line.slice(3).trim() || "unknown";
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const file = line.trim();
|
|
42
|
+
if (!file || file.includes("\0"))
|
|
43
|
+
continue;
|
|
44
|
+
bucket.push(file);
|
|
45
|
+
commitsByFile.set(file, (commitsByFile.get(file) ?? 0) + 1);
|
|
46
|
+
if (!authorsByFile.has(file))
|
|
47
|
+
authorsByFile.set(file, new Set());
|
|
48
|
+
authorsByFile.get(file).add(currentAuthor);
|
|
49
|
+
}
|
|
50
|
+
if (bucket.length)
|
|
51
|
+
commitFiles.push(bucket);
|
|
52
|
+
const hotspots = [...commitsByFile.entries()]
|
|
53
|
+
.map(([path, commits]) => {
|
|
54
|
+
const authors = [...(authorsByFile.get(path) ?? [])].sort();
|
|
55
|
+
return {
|
|
56
|
+
path,
|
|
57
|
+
commits,
|
|
58
|
+
authors,
|
|
59
|
+
busFactor: authors.length,
|
|
60
|
+
method: "count of file appearances in last ≤200 commits; busFactor=distinct authors in that window",
|
|
61
|
+
};
|
|
62
|
+
})
|
|
63
|
+
.sort((a, b) => b.commits - a.commits || a.path.localeCompare(b.path))
|
|
64
|
+
.slice(0, 50);
|
|
65
|
+
const pairCounts = new Map();
|
|
66
|
+
for (const files of commitFiles) {
|
|
67
|
+
const uniq = [...new Set(files)].sort();
|
|
68
|
+
for (let i = 0; i < uniq.length; i += 1) {
|
|
69
|
+
for (let j = i + 1; j < Math.min(uniq.length, i + 8); j += 1) {
|
|
70
|
+
const key = `${uniq[i]}||${uniq[j]}`;
|
|
71
|
+
pairCounts.set(key, (pairCounts.get(key) ?? 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const coChanges = [...pairCounts.entries()]
|
|
76
|
+
.filter(([, n]) => n >= 2)
|
|
77
|
+
.map(([key, together]) => {
|
|
78
|
+
const [a, b] = key.split("||");
|
|
79
|
+
return {
|
|
80
|
+
a: a,
|
|
81
|
+
b: b,
|
|
82
|
+
together,
|
|
83
|
+
method: "pair co-occurrence in same commit (window ≤200; capped pairs per commit)",
|
|
84
|
+
};
|
|
85
|
+
})
|
|
86
|
+
.sort((x, y) => y.together - x.together)
|
|
87
|
+
.slice(0, 40);
|
|
88
|
+
return {
|
|
89
|
+
root,
|
|
90
|
+
gitAvailable: true,
|
|
91
|
+
hotspots,
|
|
92
|
+
coChanges,
|
|
93
|
+
limitations: [
|
|
94
|
+
"Hotspots use recent commit window only (not full history)",
|
|
95
|
+
"Bus factor is author cardinality in window — not organizational coverage",
|
|
96
|
+
"Bug-fix history is not classified in this MVP",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function deadCodeCategory(options) {
|
|
101
|
+
if (options.frameworkEntry) {
|
|
102
|
+
return {
|
|
103
|
+
category: "framework-discovered",
|
|
104
|
+
confidence: 0.4,
|
|
105
|
+
explanation: "Looks like a framework entry — do not mark dead without framework adapter proof",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (options.dynamicHint) {
|
|
109
|
+
return {
|
|
110
|
+
category: "dynamic-reference-risk",
|
|
111
|
+
confidence: 0.3,
|
|
112
|
+
explanation: "Dynamic/reflection-style reference risk",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (options.exported && !options.referenced) {
|
|
116
|
+
return {
|
|
117
|
+
category: "public-api",
|
|
118
|
+
confidence: 0.55,
|
|
119
|
+
explanation: "Exported but no static references observed — may be public API",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!options.exported && !options.referenced) {
|
|
123
|
+
return {
|
|
124
|
+
category: "possibly-unused",
|
|
125
|
+
confidence: 0.5,
|
|
126
|
+
explanation: "No static references; not confirmed unreachable across all loaders",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (!options.exported && options.referenced) {
|
|
130
|
+
return { category: "unknown", confidence: 0.2, explanation: "Referenced locally" };
|
|
131
|
+
}
|
|
132
|
+
return { category: "unknown", confidence: 0.2, explanation: "Insufficient evidence" };
|
|
133
|
+
}
|
|
134
|
+
export function stableMetricId(kind, path) {
|
|
135
|
+
return `git_${createHash("sha1").update(`${kind}:${path}`).digest("hex").slice(0, 12)}`;
|
|
136
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RepositoryGraph } from "../../platform/types.js";
|
|
2
|
+
export type GraphBuilderMode = "regex" | "typescript-ast" | "auto";
|
|
3
|
+
export declare function nodeId(kind: string, key: string): string;
|
|
4
|
+
export declare function listTsFiles(root: string, limit?: number): Promise<string[]>;
|
|
5
|
+
/**
|
|
6
|
+
* TypeScript compiler API based graph enrichment.
|
|
7
|
+
* Falls back to regex graph when mode=regex or on failure.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildIntelligenceGraph(options: {
|
|
10
|
+
root: string;
|
|
11
|
+
mode?: GraphBuilderMode;
|
|
12
|
+
}): Promise<RepositoryGraph & {
|
|
13
|
+
builder: GraphBuilderMode;
|
|
14
|
+
astFilesParsed: number;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
6
|
+
import { buildRepositoryGraph } from "../../platform/graph/build.js";
|
|
7
|
+
import { CONTRACTS_VERSION } from "../../contracts/index.js";
|
|
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) {
|
|
17
|
+
return `${kind}:${createHash("sha1").update(key).digest("hex").slice(0, 12)}`;
|
|
18
|
+
}
|
|
19
|
+
export async function listTsFiles(root, limit = 400) {
|
|
20
|
+
const out = [];
|
|
21
|
+
const skip = new Set(["node_modules", ".git", "dist", "coverage", ".agentdoctor", "vendor"]);
|
|
22
|
+
async function walk(dir) {
|
|
23
|
+
if (out.length >= limit)
|
|
24
|
+
return;
|
|
25
|
+
let entries;
|
|
26
|
+
try {
|
|
27
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const e of entries) {
|
|
33
|
+
if (skip.has(e.name))
|
|
34
|
+
continue;
|
|
35
|
+
const abs = path.join(dir, e.name);
|
|
36
|
+
if (e.isSymbolicLink())
|
|
37
|
+
continue;
|
|
38
|
+
if (e.isDirectory()) {
|
|
39
|
+
await walk(abs);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (/\.(ts|tsx|mts|cts)$/i.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
43
|
+
out.push(abs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await walk(root);
|
|
48
|
+
return out.sort();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* TypeScript compiler API based graph enrichment.
|
|
52
|
+
* Falls back to regex graph when mode=regex or on failure.
|
|
53
|
+
*/
|
|
54
|
+
export async function buildIntelligenceGraph(options) {
|
|
55
|
+
const root = resolveRepoRoot(options.root);
|
|
56
|
+
const mode = options.mode ?? "auto";
|
|
57
|
+
if (mode === "regex") {
|
|
58
|
+
const g = await buildRepositoryGraph(root);
|
|
59
|
+
return { ...g, builder: "regex", astFilesParsed: 0 };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const files = await listTsFiles(root);
|
|
63
|
+
if (files.length === 0) {
|
|
64
|
+
const g = await buildRepositoryGraph(root);
|
|
65
|
+
return {
|
|
66
|
+
...g,
|
|
67
|
+
builder: "regex",
|
|
68
|
+
astFilesParsed: 0,
|
|
69
|
+
limitations: [
|
|
70
|
+
...g.limitations,
|
|
71
|
+
"No TypeScript sources found for AST builder; used regex fallback",
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const program = ts.createProgram({
|
|
76
|
+
rootNames: files,
|
|
77
|
+
options: {
|
|
78
|
+
allowJs: false,
|
|
79
|
+
target: ts.ScriptTarget.ES2022,
|
|
80
|
+
module: ts.ModuleKind.ESNext,
|
|
81
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
82
|
+
skipLibCheck: true,
|
|
83
|
+
noEmit: true,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const nodes = [];
|
|
87
|
+
const edges = [];
|
|
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);
|
|
93
|
+
const pushNode = (n) => {
|
|
94
|
+
if (seen.has(n.id))
|
|
95
|
+
return;
|
|
96
|
+
seen.add(n.id);
|
|
97
|
+
nodes.push(n);
|
|
98
|
+
};
|
|
99
|
+
for (const sf of program.getSourceFiles()) {
|
|
100
|
+
if (sf.isDeclarationFile)
|
|
101
|
+
continue;
|
|
102
|
+
if (!fileSet.has(normalizeFsPath(sf.fileName)))
|
|
103
|
+
continue;
|
|
104
|
+
const rel = toPosixRelative(root, sf.fileName);
|
|
105
|
+
const fileId = nodeId("file", rel);
|
|
106
|
+
pushNode({ id: fileId, kind: "file", label: path.basename(rel), path: rel });
|
|
107
|
+
const visit = (node) => {
|
|
108
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
109
|
+
const name = node.name.text;
|
|
110
|
+
pushNode({
|
|
111
|
+
id: nodeId("function", `${rel}:${name}`),
|
|
112
|
+
kind: "function",
|
|
113
|
+
label: name,
|
|
114
|
+
path: rel,
|
|
115
|
+
meta: { parser: "typescript-ast", analysisVersion: CONTRACTS_VERSION },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
119
|
+
const name = node.name.text;
|
|
120
|
+
const classId = nodeId("class", `${rel}:${name}`);
|
|
121
|
+
pushNode({
|
|
122
|
+
id: classId,
|
|
123
|
+
kind: "class",
|
|
124
|
+
label: name,
|
|
125
|
+
path: rel,
|
|
126
|
+
meta: { parser: "typescript-ast" },
|
|
127
|
+
});
|
|
128
|
+
if (node.heritageClauses) {
|
|
129
|
+
for (const h of node.heritageClauses) {
|
|
130
|
+
for (const t of h.types) {
|
|
131
|
+
const label = t.expression.getText(sf);
|
|
132
|
+
const toId = nodeId("dependency", `${rel}->${label}`);
|
|
133
|
+
pushNode({ id: toId, kind: "dependency", label, path: rel });
|
|
134
|
+
edges.push({
|
|
135
|
+
id: nodeId("edge", `${classId}->${toId}:${h.token}`),
|
|
136
|
+
from: classId,
|
|
137
|
+
to: toId,
|
|
138
|
+
kind: h.token === ts.SyntaxKind.ExtendsKeyword ? "extends" : "implements",
|
|
139
|
+
evidence: "verified",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
146
|
+
pushNode({
|
|
147
|
+
id: nodeId("interface", `${rel}:${node.name.text}`),
|
|
148
|
+
kind: "module",
|
|
149
|
+
label: node.name.text,
|
|
150
|
+
path: rel,
|
|
151
|
+
meta: { symbolKind: "interface", parser: "typescript-ast" },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (ts.isImportDeclaration(node) &&
|
|
155
|
+
node.moduleSpecifier &&
|
|
156
|
+
ts.isStringLiteral(node.moduleSpecifier)) {
|
|
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).
|
|
165
|
+
const toId = nodeId("dependency", `${rel}->${spec}`);
|
|
166
|
+
pushNode({
|
|
167
|
+
id: toId,
|
|
168
|
+
kind: "dependency",
|
|
169
|
+
label: spec,
|
|
170
|
+
path: rel,
|
|
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
|
+
},
|
|
178
|
+
});
|
|
179
|
+
edges.push({
|
|
180
|
+
id: nodeId("edge", `${fileId}->${toId}`),
|
|
181
|
+
from: fileId,
|
|
182
|
+
to: toId,
|
|
183
|
+
kind: "imports",
|
|
184
|
+
evidence: evidenceForConfidence(resolution.confidence),
|
|
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
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (ts.isCallExpression(node) &&
|
|
206
|
+
ts.isIdentifier(node.expression) &&
|
|
207
|
+
node.expression.text.length > 1) {
|
|
208
|
+
const callee = node.expression.text;
|
|
209
|
+
const toId = nodeId("function", `call:${rel}:${callee}`);
|
|
210
|
+
pushNode({
|
|
211
|
+
id: toId,
|
|
212
|
+
kind: "function",
|
|
213
|
+
label: callee,
|
|
214
|
+
path: rel,
|
|
215
|
+
meta: { callSite: true, parser: "typescript-ast" },
|
|
216
|
+
});
|
|
217
|
+
edges.push({
|
|
218
|
+
id: nodeId("edge", `${fileId}->call:${callee}:${node.getStart(sf)}`),
|
|
219
|
+
from: fileId,
|
|
220
|
+
to: toId,
|
|
221
|
+
kind: "calls",
|
|
222
|
+
evidence: "inferred",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
ts.forEachChild(node, visit);
|
|
226
|
+
};
|
|
227
|
+
visit(sf);
|
|
228
|
+
}
|
|
229
|
+
nodes.sort((a, b) => a.id.localeCompare(b.id));
|
|
230
|
+
edges.sort((a, b) => a.id.localeCompare(b.id));
|
|
231
|
+
return {
|
|
232
|
+
root,
|
|
233
|
+
generatedAt: new Date().toISOString(),
|
|
234
|
+
nodes,
|
|
235
|
+
edges,
|
|
236
|
+
builder: "typescript-ast",
|
|
237
|
+
astFilesParsed: files.length,
|
|
238
|
+
limitations: [
|
|
239
|
+
"TypeScript AST builder uses the TypeScript compiler API for .ts/.tsx only",
|
|
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",
|
|
242
|
+
"Non-TypeScript languages fall back to regex graph when requested via auto+empty TS set",
|
|
243
|
+
`analysisVersion=${CONTRACTS_VERSION}`,
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
const g = await buildRepositoryGraph(root);
|
|
249
|
+
return {
|
|
250
|
+
...g,
|
|
251
|
+
builder: "regex",
|
|
252
|
+
astFilesParsed: 0,
|
|
253
|
+
limitations: [
|
|
254
|
+
...g.limitations,
|
|
255
|
+
`TypeScript AST builder failed; regex fallback used (${error instanceof Error ? error.message : String(error)})`,
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -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>;
|