@praneeth_54/agentdoctor 1.1.0 → 2.0.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 (170) hide show
  1. package/CHANGELOG.md +119 -5
  2. package/README.md +165 -332
  3. package/dist/agents/aider/adapter.d.ts +1 -0
  4. package/dist/agents/aider/adapter.js +1 -0
  5. package/dist/agents/aider/detector.d.ts +14 -0
  6. package/dist/agents/aider/detector.js +120 -0
  7. package/dist/agents/copilot/adapter.d.ts +1 -0
  8. package/dist/agents/copilot/adapter.js +1 -0
  9. package/dist/agents/copilot/detector.d.ts +13 -0
  10. package/dist/agents/copilot/detector.js +113 -0
  11. package/dist/agents/gemini/adapter.d.ts +1 -0
  12. package/dist/agents/gemini/adapter.js +1 -0
  13. package/dist/agents/gemini/detector.d.ts +13 -0
  14. package/dist/agents/gemini/detector.js +151 -0
  15. package/dist/agents/registry.js +13 -1
  16. package/dist/agents/types.d.ts +1 -1
  17. package/dist/agents/windsurf/adapter.d.ts +1 -0
  18. package/dist/agents/windsurf/adapter.js +1 -0
  19. package/dist/agents/windsurf/detector.d.ts +14 -0
  20. package/dist/agents/windsurf/detector.js +119 -0
  21. package/dist/architecture/c4.d.ts +22 -0
  22. package/dist/architecture/c4.js +62 -0
  23. package/dist/cli/commands/brain.d.ts +12 -0
  24. package/dist/cli/commands/brain.js +162 -0
  25. package/dist/cli/commands/complete.d.ts +23 -0
  26. package/dist/cli/commands/complete.js +232 -0
  27. package/dist/cli/commands/explain.js +1 -1
  28. package/dist/cli/commands/fix.js +22 -2
  29. package/dist/cli/commands/mcp.d.ts +8 -0
  30. package/dist/cli/commands/mcp.js +14 -0
  31. package/dist/cli/commands/platform.d.ts +15 -0
  32. package/dist/cli/commands/platform.js +191 -0
  33. package/dist/cli/commands/v2.d.ts +55 -0
  34. package/dist/cli/commands/v2.js +278 -0
  35. package/dist/cli/program.js +809 -2
  36. package/dist/constants.d.ts +3 -2
  37. package/dist/constants.js +5 -1
  38. package/dist/contracts/adapters.d.ts +7 -0
  39. package/dist/contracts/adapters.js +60 -0
  40. package/dist/contracts/index.d.ts +162 -0
  41. package/dist/contracts/index.js +18 -0
  42. package/dist/core/baseline/store.d.ts +48 -0
  43. package/dist/core/baseline/store.js +163 -0
  44. package/dist/core/brain-cli/service.d.ts +26 -0
  45. package/dist/core/brain-cli/service.js +175 -0
  46. package/dist/core/brain-product/init.d.ts +45 -0
  47. package/dist/core/brain-product/init.js +232 -0
  48. package/dist/core/changes/analyze.d.ts +41 -0
  49. package/dist/core/changes/analyze.js +223 -0
  50. package/dist/core/context-health/analyze.d.ts +15 -0
  51. package/dist/core/context-health/analyze.js +234 -0
  52. package/dist/core/fix/apply.d.ts +21 -4
  53. package/dist/core/fix/apply.js +118 -18
  54. package/dist/core/fix/backup.d.ts +30 -0
  55. package/dist/core/fix/backup.js +193 -0
  56. package/dist/core/fix/plan.d.ts +6 -3
  57. package/dist/core/fix/plan.js +113 -9
  58. package/dist/core/fix/render.d.ts +2 -0
  59. package/dist/core/fix/render.js +36 -21
  60. package/dist/core/fix/run.js +5 -1
  61. package/dist/core/fix/safe-target.d.ts +14 -0
  62. package/dist/core/fix/safe-target.js +81 -0
  63. package/dist/core/fix/types.d.ts +12 -1
  64. package/dist/core/fix/types.js +2 -0
  65. package/dist/core/fix/writers/claude-settings.js +3 -1
  66. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  67. package/dist/core/fix/writers/codex-config.js +24 -3
  68. package/dist/core/fix/writers/cursorignore.js +3 -1
  69. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  70. package/dist/core/fix/writers/simple-ignore.js +68 -0
  71. package/dist/core/monorepo/detect.d.ts +13 -0
  72. package/dist/core/monorepo/detect.js +121 -0
  73. package/dist/core/rules/build-context.js +22 -3
  74. package/dist/core/rules/context/generated-directory.js +11 -1
  75. package/dist/core/rules/context/large-instruction-file.js +6 -0
  76. package/dist/core/rules/context/large-log-file.js +16 -1
  77. package/dist/core/rules/ignore.d.ts +11 -1
  78. package/dist/core/rules/ignore.js +22 -1
  79. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  80. package/dist/core/rules/security/env-file-exposure.js +23 -2
  81. package/dist/core/rules/security/private-key-file.js +1 -1
  82. package/dist/core/scanner/scan.js +1 -1
  83. package/dist/core/schemas/validate.d.ts +6 -0
  84. package/dist/core/schemas/validate.js +62 -0
  85. package/dist/core/scoring/compute-scores.d.ts +1 -1
  86. package/dist/core/scoring/compute-scores.js +5 -1
  87. package/dist/core/scoring/placeholder.d.ts +1 -1
  88. package/dist/core/scoring/placeholder.js +5 -1
  89. package/dist/core/secrets/scan.d.ts +23 -0
  90. package/dist/core/secrets/scan.js +172 -0
  91. package/dist/dashboard/server.d.ts +24 -0
  92. package/dist/dashboard/server.js +386 -0
  93. package/dist/enforcement/runner.d.ts +23 -0
  94. package/dist/enforcement/runner.js +56 -0
  95. package/dist/index.d.ts +13 -0
  96. package/dist/index.js +12 -0
  97. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  98. package/dist/integrations/github/pr-analyze.js +98 -0
  99. package/dist/integrations/local-ai/provider.d.ts +45 -0
  100. package/dist/integrations/local-ai/provider.js +104 -0
  101. package/dist/intelligence/git/analyze.d.ts +34 -0
  102. package/dist/intelligence/git/analyze.js +136 -0
  103. package/dist/intelligence/graph/build.d.ts +13 -0
  104. package/dist/intelligence/graph/build.js +215 -0
  105. package/dist/knowledge/store.d.ts +25 -0
  106. package/dist/knowledge/store.js +91 -0
  107. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  108. package/dist/mcp/agentdoctor/server.js +67 -0
  109. package/dist/mcp/brain/session.js +7 -7
  110. package/dist/mcp/intelligence/handlers.d.ts +11 -0
  111. package/dist/mcp/intelligence/handlers.js +175 -0
  112. package/dist/mcp/intelligence/path-safety.d.ts +11 -0
  113. package/dist/mcp/intelligence/path-safety.js +82 -0
  114. package/dist/mcp/intelligence/registry.d.ts +8 -0
  115. package/dist/mcp/intelligence/registry.js +160 -0
  116. package/dist/ops/health.d.ts +18 -0
  117. package/dist/ops/health.js +51 -0
  118. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  119. package/dist/platform/ai-quality/analyze.js +65 -0
  120. package/dist/platform/architecture/drift.d.ts +17 -0
  121. package/dist/platform/architecture/drift.js +74 -0
  122. package/dist/platform/auth/local.d.ts +15 -0
  123. package/dist/platform/auth/local.js +58 -0
  124. package/dist/platform/context-security/analyze.d.ts +6 -0
  125. package/dist/platform/context-security/analyze.js +97 -0
  126. package/dist/platform/firewall/evaluate.d.ts +54 -0
  127. package/dist/platform/firewall/evaluate.js +308 -0
  128. package/dist/platform/graph/build.d.ts +6 -0
  129. package/dist/platform/graph/build.js +193 -0
  130. package/dist/platform/health/analyze.d.ts +7 -0
  131. package/dist/platform/health/analyze.js +186 -0
  132. package/dist/platform/index.d.ts +27 -0
  133. package/dist/platform/index.js +87 -0
  134. package/dist/platform/knowledge/analyze.d.ts +20 -0
  135. package/dist/platform/knowledge/analyze.js +96 -0
  136. package/dist/platform/provenance/build.d.ts +44 -0
  137. package/dist/platform/provenance/build.js +65 -0
  138. package/dist/platform/readiness/scorecard.d.ts +5 -0
  139. package/dist/platform/readiness/scorecard.js +98 -0
  140. package/dist/platform/refactor/impact.d.ts +20 -0
  141. package/dist/platform/refactor/impact.js +60 -0
  142. package/dist/platform/reports/export.d.ts +12 -0
  143. package/dist/platform/reports/export.js +92 -0
  144. package/dist/platform/security/redact.d.ts +15 -0
  145. package/dist/platform/security/redact.js +87 -0
  146. package/dist/platform/sessions/store.d.ts +32 -0
  147. package/dist/platform/sessions/store.js +97 -0
  148. package/dist/platform/store.d.ts +6 -0
  149. package/dist/platform/store.js +57 -0
  150. package/dist/platform/test-impact/analyze.d.ts +32 -0
  151. package/dist/platform/test-impact/analyze.js +164 -0
  152. package/dist/platform/time-machine/compare.d.ts +17 -0
  153. package/dist/platform/time-machine/compare.js +62 -0
  154. package/dist/platform/tokens/plan.d.ts +28 -0
  155. package/dist/platform/tokens/plan.js +89 -0
  156. package/dist/platform/types.d.ts +111 -0
  157. package/dist/platform/types.js +5 -0
  158. package/dist/plugins/runtime.d.ts +36 -0
  159. package/dist/plugins/runtime.js +101 -0
  160. package/dist/plugins/sdk.d.ts +29 -0
  161. package/dist/plugins/sdk.js +114 -0
  162. package/dist/policy/packs.d.ts +9 -0
  163. package/dist/policy/packs.js +59 -0
  164. package/dist/reporters/terminal/report.js +38 -5
  165. package/dist/storage/provider.d.ts +35 -0
  166. package/dist/storage/provider.js +103 -0
  167. package/dist/team/auth.d.ts +35 -0
  168. package/dist/team/auth.js +70 -0
  169. package/dist/types/index.d.ts +5 -1
  170. package/package.json +14 -7
@@ -0,0 +1,82 @@
1
+ import path from "node:path";
2
+ import { isPathInsideRoot, sanitizeForOutput } from "../../utils/path.js";
3
+ /**
4
+ * Decode a single layer of URI encoding. Rejects malformed sequences by returning null.
5
+ */
6
+ export function tryDecodeUriComponent(value) {
7
+ try {
8
+ return decodeURIComponent(value);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ /**
15
+ * Validate a user-supplied repository-relative or in-repo target.
16
+ * Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
17
+ * or null when the target is treated as a non-path identifier (graph id / label).
18
+ * Throws a safe Error (no outside path leakage) when a path-like target escapes the root.
19
+ */
20
+ export function assertSafeRepoTarget(root, rawTarget) {
21
+ if (typeof rawTarget !== "string" || rawTarget.includes("\0")) {
22
+ throw new Error("invalid target");
23
+ }
24
+ const cleaned = sanitizeForOutput(rawTarget).trim();
25
+ if (!cleaned) {
26
+ throw new Error("invalid target");
27
+ }
28
+ if (cleaned.includes("\0")) {
29
+ throw new Error("invalid target");
30
+ }
31
+ const decoded = tryDecodeUriComponent(cleaned);
32
+ if (decoded === null) {
33
+ throw new Error("invalid target");
34
+ }
35
+ const candidates = decoded === cleaned ? [cleaned] : [cleaned, decoded];
36
+ for (const candidate of candidates) {
37
+ if (candidate.includes("\0")) {
38
+ throw new Error("invalid target");
39
+ }
40
+ const looksLikePath = path.isAbsolute(candidate) ||
41
+ candidate.includes("..") ||
42
+ candidate.includes("/") ||
43
+ candidate.includes("\\") ||
44
+ /^[A-Za-z]:[\\/]/.test(candidate);
45
+ if (!looksLikePath) {
46
+ continue;
47
+ }
48
+ // Reject any .. segment forms before resolve (prevents src/x/../../etc tricks).
49
+ const normalizedSlashes = candidate.replace(/\\/g, "/");
50
+ if (normalizedSlashes === ".." ||
51
+ normalizedSlashes.includes("/../") ||
52
+ normalizedSlashes.startsWith("../") ||
53
+ normalizedSlashes.endsWith("/..") ||
54
+ normalizedSlashes.includes("/..") ||
55
+ /(^|\/)\.\.($|\/)/.test(normalizedSlashes)) {
56
+ throw new Error("path escapes repository root");
57
+ }
58
+ // Always reject Windows drive-absolute forms (even on POSIX hosts).
59
+ if (/^[A-Za-z]:[\\/]/.test(candidate)) {
60
+ throw new Error("path escapes repository root");
61
+ }
62
+ // Resolve relative to root; absolute candidates resolve to themselves.
63
+ const resolved = path.isAbsolute(candidate)
64
+ ? path.resolve(candidate)
65
+ : path.resolve(root, candidate);
66
+ if (!isPathInsideRoot(root, resolved)) {
67
+ throw new Error("path escapes repository root");
68
+ }
69
+ }
70
+ // Prefer decoded form for relative path returns when path-like.
71
+ const primary = candidates[candidates.length - 1];
72
+ const looksLikePath = path.isAbsolute(primary) ||
73
+ primary.includes("..") ||
74
+ primary.includes("/") ||
75
+ primary.includes("\\") ||
76
+ /^[A-Za-z]:[\\/]/.test(primary);
77
+ if (!looksLikePath) {
78
+ return null;
79
+ }
80
+ const resolved = path.isAbsolute(primary) ? path.resolve(primary) : path.resolve(root, primary);
81
+ return path.relative(root, resolved).split(path.sep).join("/");
82
+ }
@@ -0,0 +1,8 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const INTELLIGENCE_MCP_TOOL_NAMES: readonly ["repo_overview", "codebase_search", "symbol_lookup", "dependency_lookup", "call_graph_lookup", "test_impact", "refactor_impact", "code_health", "architecture_info", "knowledge_retrieve", "policy_evaluate"];
3
+ export type IntelligenceMcpToolName = (typeof INTELLIGENCE_MCP_TOOL_NAMES)[number];
4
+ export declare function listIntelligenceMcpTools(): Tool[];
5
+ export declare function invokeIntelligenceMcpTool(root: string, name: string, args: Record<string, unknown>): Promise<{
6
+ structured: unknown;
7
+ isError: boolean;
8
+ }>;
@@ -0,0 +1,160 @@
1
+ import { handleArchitectureTool, handleCallGraphLookup, handleCodeHealthTool, handleCodebaseSearch, handleDependencyLookup, handleKnowledgeTool, handlePolicyEvalTool, handleRefactorImpactTool, handleRepoOverview, handleSymbolLookup, handleTestImpactTool, } from "./handlers.js";
2
+ export const INTELLIGENCE_MCP_TOOL_NAMES = [
3
+ "repo_overview",
4
+ "codebase_search",
5
+ "symbol_lookup",
6
+ "dependency_lookup",
7
+ "call_graph_lookup",
8
+ "test_impact",
9
+ "refactor_impact",
10
+ "code_health",
11
+ "architecture_info",
12
+ "knowledge_retrieve",
13
+ "policy_evaluate",
14
+ ];
15
+ const emptyObjectSchema = {
16
+ type: "object",
17
+ properties: {},
18
+ additionalProperties: false,
19
+ };
20
+ export function listIntelligenceMcpTools() {
21
+ return [
22
+ {
23
+ name: "repo_overview",
24
+ description: "READ: Repository intelligence overview (graph builder, node/edge counts, limitations). No shell execution.",
25
+ inputSchema: emptyObjectSchema,
26
+ },
27
+ {
28
+ name: "codebase_search",
29
+ description: "READ: Search graph nodes by label/path substring. Bounded to 50 hits.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: { query: { type: "string" } },
33
+ required: ["query"],
34
+ additionalProperties: false,
35
+ },
36
+ },
37
+ {
38
+ name: "symbol_lookup",
39
+ description: "READ: Lookup symbols/functions/classes/interfaces by name.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: { name: { type: "string" } },
43
+ required: ["name"],
44
+ additionalProperties: false,
45
+ },
46
+ },
47
+ {
48
+ name: "dependency_lookup",
49
+ description: "READ: Edges connected to a graph node (id/path/label).",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: { target: { type: "string" } },
53
+ required: ["target"],
54
+ additionalProperties: false,
55
+ },
56
+ },
57
+ {
58
+ name: "call_graph_lookup",
59
+ description: "READ: Call edges involving a symbol (best-effort; AST when available).",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: { symbol: { type: "string" } },
63
+ required: ["symbol"],
64
+ additionalProperties: false,
65
+ },
66
+ },
67
+ {
68
+ name: "test_impact",
69
+ description: "READ: Test-impact analysis for current git changes. Coverage-backed mapping not bundled.",
70
+ inputSchema: emptyObjectSchema,
71
+ },
72
+ {
73
+ name: "refactor_impact",
74
+ description: "READ: Rename/refactor blast radius for a symbol.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: { symbol: { type: "string" } },
78
+ required: ["symbol"],
79
+ additionalProperties: false,
80
+ },
81
+ },
82
+ {
83
+ name: "code_health",
84
+ description: "READ: Git hotspot / engineering intelligence with method disclosure.",
85
+ inputSchema: emptyObjectSchema,
86
+ },
87
+ {
88
+ name: "architecture_info",
89
+ description: "READ: C4-style views inferred from graph evidence (labeled proposed/inferred).",
90
+ inputSchema: emptyObjectSchema,
91
+ },
92
+ {
93
+ name: "knowledge_retrieve",
94
+ description: "READ: Governed knowledge retrieval. Abstains when no approved authoritative record matches.",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: { query: { type: "string" } },
98
+ additionalProperties: false,
99
+ },
100
+ },
101
+ {
102
+ name: "policy_evaluate",
103
+ description: "READ: Evaluate a shell command against local policy. Never executes. Always executionResult=not-executed.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: { command: { type: "string" } },
107
+ required: ["command"],
108
+ additionalProperties: false,
109
+ },
110
+ },
111
+ ];
112
+ }
113
+ export async function invokeIntelligenceMcpTool(root, name, args) {
114
+ try {
115
+ switch (name) {
116
+ case "repo_overview":
117
+ return { structured: await handleRepoOverview(root), isError: false };
118
+ case "codebase_search":
119
+ return { structured: await handleCodebaseSearch(root, args), isError: false };
120
+ case "symbol_lookup":
121
+ return { structured: await handleSymbolLookup(root, args), isError: false };
122
+ case "dependency_lookup":
123
+ return { structured: await handleDependencyLookup(root, args), isError: false };
124
+ case "call_graph_lookup":
125
+ return { structured: await handleCallGraphLookup(root, args), isError: false };
126
+ case "test_impact":
127
+ return { structured: await handleTestImpactTool(root), isError: false };
128
+ case "refactor_impact":
129
+ return { structured: await handleRefactorImpactTool(root, args), isError: false };
130
+ case "code_health":
131
+ return { structured: await handleCodeHealthTool(root), isError: false };
132
+ case "architecture_info":
133
+ return { structured: await handleArchitectureTool(root), isError: false };
134
+ case "knowledge_retrieve":
135
+ return { structured: await handleKnowledgeTool(root, args), isError: false };
136
+ case "policy_evaluate":
137
+ return { structured: await handlePolicyEvalTool(root, args), isError: false };
138
+ default:
139
+ return {
140
+ structured: {
141
+ ok: false,
142
+ error: { code: "invalid_argument", message: `unknown intelligence tool: ${name}` },
143
+ },
144
+ isError: true,
145
+ };
146
+ }
147
+ }
148
+ catch (error) {
149
+ return {
150
+ structured: {
151
+ ok: false,
152
+ error: {
153
+ code: "internal",
154
+ message: error instanceof Error ? error.message : String(error),
155
+ },
156
+ },
157
+ isError: true,
158
+ };
159
+ }
160
+ }
@@ -0,0 +1,18 @@
1
+ import { DEFAULT_FEATURE_FLAGS } from "../contracts/index.js";
2
+ export interface OpsHealthReport {
3
+ ok: boolean;
4
+ version: string;
5
+ contractsVersion: string;
6
+ root: string;
7
+ featureFlags: typeof DEFAULT_FEATURE_FLAGS;
8
+ checks: Array<{
9
+ id: string;
10
+ ok: boolean;
11
+ detail: string;
12
+ }>;
13
+ timestamp: string;
14
+ }
15
+ /**
16
+ * Local ops health probe — no network calls, no agent interception.
17
+ */
18
+ export declare function collectOpsHealth(rootInput?: string): Promise<OpsHealthReport>;
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { PACKAGE_VERSION } from "../constants.js";
4
+ import { CONTRACTS_VERSION, DEFAULT_FEATURE_FLAGS } from "../contracts/index.js";
5
+ import { resolveRepoRoot } from "../utils/path.js";
6
+ /**
7
+ * Local ops health probe — no network calls, no agent interception.
8
+ */
9
+ export async function collectOpsHealth(rootInput) {
10
+ const root = resolveRepoRoot(rootInput ?? process.cwd());
11
+ const checks = [];
12
+ try {
13
+ await fs.access(root);
14
+ checks.push({ id: "root-readable", ok: true, detail: root });
15
+ }
16
+ catch {
17
+ checks.push({ id: "root-readable", ok: false, detail: "cannot access root" });
18
+ }
19
+ const agentdoctorDir = path.join(root, ".agentdoctor");
20
+ try {
21
+ await fs.access(agentdoctorDir);
22
+ checks.push({ id: "agentdoctor-dir", ok: true, detail: ".agentdoctor present" });
23
+ }
24
+ catch {
25
+ checks.push({
26
+ id: "agentdoctor-dir",
27
+ ok: true,
28
+ detail: ".agentdoctor absent (created on first scan/init)",
29
+ });
30
+ }
31
+ checks.push({
32
+ id: "evaluate-only-firewall",
33
+ ok: true,
34
+ detail: "Action Policy Evaluator remains evaluate-only by default",
35
+ });
36
+ checks.push({
37
+ id: "local-dev-auth",
38
+ ok: true,
39
+ detail: "Team auth is local-dev only unless external IdP is configured (not bundled)",
40
+ });
41
+ const ok = checks.every((c) => c.ok);
42
+ return {
43
+ ok,
44
+ version: PACKAGE_VERSION,
45
+ contractsVersion: CONTRACTS_VERSION,
46
+ root,
47
+ featureFlags: DEFAULT_FEATURE_FLAGS,
48
+ checks,
49
+ timestamp: new Date().toISOString(),
50
+ };
51
+ }
@@ -0,0 +1,9 @@
1
+ import type { PlatformFinding } from "../types.js";
2
+ /**
3
+ * Module M — AI code quality analysis over git diff (when available).
4
+ * AI attribution is unknown unless provided.
5
+ */
6
+ export declare function analyzeAiChangeQuality(options: {
7
+ root: string;
8
+ aiAssisted?: boolean | "unknown";
9
+ }): Promise<PlatformFinding[]>;
@@ -0,0 +1,65 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { resolveRepoRoot } from "../../utils/path.js";
4
+ /**
5
+ * Module M — AI code quality analysis over git diff (when available).
6
+ * AI attribution is unknown unless provided.
7
+ */
8
+ export async function analyzeAiChangeQuality(options) {
9
+ const root = resolveRepoRoot(options.root);
10
+ const attribution = options.aiAssisted === true ? "ai-assisted" : "unknown";
11
+ const findings = [];
12
+ const diff = spawnSync("git", ["diff", "--unified=0"], {
13
+ cwd: root,
14
+ encoding: "utf8",
15
+ maxBuffer: 4 * 1024 * 1024,
16
+ });
17
+ if (diff.status !== 0) {
18
+ return [
19
+ {
20
+ id: "aiq_no_diff",
21
+ module: "ai-quality",
22
+ severity: "info",
23
+ title: "No git diff available",
24
+ message: "Cannot analyze working-tree changes",
25
+ recommendation: "Run inside a git repo with local changes",
26
+ confidence: 1,
27
+ evidence: [{ kind: "unknown", detail: "git diff unavailable" }],
28
+ },
29
+ ];
30
+ }
31
+ const text = diff.stdout ?? "";
32
+ const added = text
33
+ .split(/\r?\n/)
34
+ .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
35
+ .map((l) => l.slice(1));
36
+ const checks = [
37
+ { re: /eval\s*\(/, title: "Added eval()", severity: "critical" },
38
+ { re: /innerHTML\s*=/, title: "Added innerHTML assignment", severity: "high" },
39
+ {
40
+ re: /password\s*=\s*['"][^'"]+['"]/i,
41
+ title: "Hardcoded password-like literal",
42
+ severity: "critical",
43
+ },
44
+ { re: /TODO|FIXME/, title: "Added TODO/FIXME in change", severity: "low" },
45
+ { re: /catch\s*\([^)]*\)\s*\{\s*\}/, title: "Added empty catch", severity: "medium" },
46
+ ];
47
+ for (const [i, line] of added.entries()) {
48
+ for (const check of checks) {
49
+ if (!check.re.test(line))
50
+ continue;
51
+ findings.push({
52
+ id: `aiq_${createHash("sha1").update(`${check.title}:${i}:${line}`).digest("hex").slice(0, 12)}`,
53
+ module: "ai-quality",
54
+ severity: check.severity,
55
+ title: check.title,
56
+ message: `In working-tree addition (attribution=${attribution}): ${line.slice(0, 120)}`,
57
+ recommendation: "Review before commit; add tests for behavioral changes",
58
+ confidence: 0.6,
59
+ evidence: [{ kind: "inferred", detail: line.slice(0, 160) }],
60
+ falsePositiveWarning: "Diff-line heuristic only",
61
+ });
62
+ }
63
+ }
64
+ return findings.sort((a, b) => a.id.localeCompare(b.id));
65
+ }
@@ -0,0 +1,17 @@
1
+ import type { PlatformFinding, RepositoryGraph } from "../types.js";
2
+ export interface ArchitectureRule {
3
+ id: string;
4
+ description: string;
5
+ forbidImportFrom?: string;
6
+ forbidImportTo?: string;
7
+ severity: PlatformFinding["severity"];
8
+ }
9
+ export interface ArchitecturePolicy {
10
+ version: "2.0";
11
+ rules: ArchitectureRule[];
12
+ }
13
+ export declare function loadArchitecturePolicy(root: string): Promise<ArchitecturePolicy>;
14
+ /**
15
+ * Module I — Architecture drift detection.
16
+ */
17
+ export declare function analyzeArchitectureDrift(rootInput: string, graph: RepositoryGraph): Promise<PlatformFinding[]>;
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { writeJsonArtifact, platformDir } from "../store.js";
6
+ const DEFAULT_POLICY = {
7
+ version: "2.0",
8
+ rules: [
9
+ {
10
+ id: "no-src-to-dist",
11
+ description: "Source should not import built dist artifacts",
12
+ forbidImportFrom: "src/",
13
+ forbidImportTo: "dist/",
14
+ severity: "high",
15
+ },
16
+ {
17
+ id: "tests-not-imported-by-src",
18
+ description: "Production source should not import test files",
19
+ forbidImportFrom: "src/",
20
+ forbidImportTo: ".test.",
21
+ severity: "medium",
22
+ },
23
+ ],
24
+ };
25
+ export async function loadArchitecturePolicy(root) {
26
+ const file = path.join(platformDir(root), "architecture-policy.json");
27
+ try {
28
+ const raw = await fs.readFile(file, "utf8");
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed.version === "2.0")
31
+ return parsed;
32
+ }
33
+ catch {
34
+ // default
35
+ }
36
+ await writeJsonArtifact(resolveRepoRoot(root), "architecture-policy.json", DEFAULT_POLICY);
37
+ return DEFAULT_POLICY;
38
+ }
39
+ /**
40
+ * Module I — Architecture drift detection.
41
+ */
42
+ export async function analyzeArchitectureDrift(rootInput, graph) {
43
+ const root = resolveRepoRoot(rootInput);
44
+ const policy = await loadArchitecturePolicy(root);
45
+ const findings = [];
46
+ for (const edge of graph.edges.filter((e) => e.kind === "imports")) {
47
+ const fromNode = graph.nodes.find((n) => n.id === edge.from);
48
+ const toLabel = graph.nodes.find((n) => n.id === edge.to)?.label ?? edge.to;
49
+ const fromPath = fromNode?.path ?? "";
50
+ for (const rule of policy.rules) {
51
+ const fromHit = rule.forbidImportFrom ? fromPath.includes(rule.forbidImportFrom) : true;
52
+ const toHit = rule.forbidImportTo ? toLabel.includes(rule.forbidImportTo) : true;
53
+ if (fromHit && toHit && rule.forbidImportFrom && rule.forbidImportTo) {
54
+ findings.push({
55
+ id: `arch_${createHash("sha1").update(`${rule.id}:${edge.id}`).digest("hex").slice(0, 12)}`,
56
+ module: "architecture-drift",
57
+ severity: rule.severity,
58
+ title: `Architecture rule violated: ${rule.id}`,
59
+ message: `${fromPath} imports ${toLabel}`,
60
+ recommendation: rule.description,
61
+ confidence: edge.evidence === "verified" ? 0.7 : 0.4,
62
+ evidence: [
63
+ {
64
+ kind: edge.evidence,
65
+ path: fromPath,
66
+ detail: `edge=${edge.kind} rule=${rule.id}`,
67
+ },
68
+ ],
69
+ });
70
+ }
71
+ }
72
+ }
73
+ return findings.sort((a, b) => a.id.localeCompare(b.id));
74
+ }
@@ -0,0 +1,15 @@
1
+ import type { LocalRole } from "../types.js";
2
+ export interface LocalAuthConfig {
3
+ /** Local-only roles map. Not a cloud IdP. */
4
+ users: Array<{
5
+ id: string;
6
+ role: LocalRole;
7
+ }>;
8
+ defaultRole: LocalRole;
9
+ }
10
+ export declare function loadLocalAuthConfig(root: string): Promise<LocalAuthConfig>;
11
+ export declare function resolveRole(config: LocalAuthConfig, userId?: string): LocalRole;
12
+ export declare function roleAtLeast(role: LocalRole, required: LocalRole): boolean;
13
+ /** Permission matrix for local dashboard/API (no network auth; spoofable via local config). */
14
+ export declare function canAccess(role: LocalRole, resource: "read-findings" | "manage-policies" | "approve-actions" | "read-audit" | "export-reports" | "admin-settings"): boolean;
15
+ export declare function ensureAuthConfig(root: string): Promise<string>;
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { platformDir, readJsonIfExists, writeJsonArtifact } from "../store.js";
3
+ import { resolveRepoRoot } from "../../utils/path.js";
4
+ const DEFAULT_CONFIG = {
5
+ defaultRole: "readonly",
6
+ users: [
7
+ { id: "local-admin", role: "admin" },
8
+ { id: "local-developer", role: "developer" },
9
+ { id: "local-reviewer", role: "reviewer" },
10
+ { id: "local-auditor", role: "auditor" },
11
+ ],
12
+ };
13
+ const ROLE_RANK = {
14
+ readonly: 0,
15
+ auditor: 1,
16
+ developer: 2,
17
+ reviewer: 3,
18
+ admin: 4,
19
+ };
20
+ export async function loadLocalAuthConfig(root) {
21
+ const file = path.join(platformDir(root), "auth.json");
22
+ const existing = await readJsonIfExists(file);
23
+ if (existing?.users && existing.defaultRole)
24
+ return existing;
25
+ await writeJsonArtifact(resolveRepoRoot(root), "auth.json", DEFAULT_CONFIG);
26
+ return DEFAULT_CONFIG;
27
+ }
28
+ export function resolveRole(config, userId) {
29
+ if (!userId)
30
+ return config.defaultRole;
31
+ return config.users.find((u) => u.id === userId)?.role ?? config.defaultRole;
32
+ }
33
+ export function roleAtLeast(role, required) {
34
+ return (ROLE_RANK[role] ?? 0) >= (ROLE_RANK[required] ?? 0);
35
+ }
36
+ /** Permission matrix for local dashboard/API (no network auth; spoofable via local config). */
37
+ export function canAccess(role, resource) {
38
+ switch (resource) {
39
+ case "read-findings":
40
+ return roleAtLeast(role, "readonly");
41
+ case "read-audit":
42
+ return roleAtLeast(role, "auditor");
43
+ case "export-reports":
44
+ return roleAtLeast(role, "developer");
45
+ case "approve-actions":
46
+ return roleAtLeast(role, "reviewer");
47
+ case "manage-policies":
48
+ return roleAtLeast(role, "admin");
49
+ case "admin-settings":
50
+ return role === "admin";
51
+ default:
52
+ return false;
53
+ }
54
+ }
55
+ export async function ensureAuthConfig(root) {
56
+ await loadLocalAuthConfig(root);
57
+ return path.join(platformDir(root), "auth.json");
58
+ }
@@ -0,0 +1,6 @@
1
+ import type { PlatformFinding } from "../types.js";
2
+ /**
3
+ * Module F — Context security / poisoning detection (pluggable pattern set).
4
+ * Does not claim perfect detection. Evidence snippets are secret-redacted.
5
+ */
6
+ export declare function analyzeContextSecurity(rootInput: string): Promise<PlatformFinding[]>;
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
5
+ import { redactEvidenceDetail } from "../security/redact.js";
6
+ const INJECTION_PATTERNS = [
7
+ {
8
+ id: "ignore-previous",
9
+ re: /ignore (all )?(previous|prior|above) (instructions|rules|policies)/i,
10
+ severity: "critical",
11
+ title: "Prompt-injection: ignore previous instructions",
12
+ },
13
+ {
14
+ id: "exfiltrate",
15
+ re: /exfiltrat|send (all )?(secrets|credentials|api keys) to/i,
16
+ severity: "critical",
17
+ title: "Suspected exfiltration instruction",
18
+ },
19
+ {
20
+ id: "bypass-policy",
21
+ re: /bypass (the )?(firewall|policy|approval|security|action policy)/i,
22
+ severity: "high",
23
+ title: "Policy-bypass instruction",
24
+ },
25
+ {
26
+ id: "hidden-html",
27
+ re: /<!--\s*(system|assistant|secret instruction)/i,
28
+ severity: "high",
29
+ title: "Hidden HTML instruction comment",
30
+ },
31
+ {
32
+ id: "social-eng",
33
+ re: /do not tell (the )?user|hide this from (the )?developer/i,
34
+ severity: "high",
35
+ title: "Social-engineering pattern",
36
+ },
37
+ {
38
+ id: "obfuscated-b64",
39
+ re: /eval\s*\(\s*atob\s*\(|Buffer\.from\([^,]+,\s*['"]base64['"]/i,
40
+ severity: "medium",
41
+ title: "Obfuscated execution pattern",
42
+ },
43
+ ];
44
+ /**
45
+ * Module F — Context security / poisoning detection (pluggable pattern set).
46
+ * Does not claim perfect detection. Evidence snippets are secret-redacted.
47
+ */
48
+ export async function analyzeContextSecurity(rootInput) {
49
+ const root = resolveRepoRoot(rootInput);
50
+ const findings = [];
51
+ const candidates = [
52
+ "AGENTS.md",
53
+ "CLAUDE.md",
54
+ "GEMINI.md",
55
+ "CONVENTIONS.md",
56
+ "README.md",
57
+ ".cursorrules",
58
+ ".github/copilot-instructions.md",
59
+ ];
60
+ for (const rel of candidates) {
61
+ const absolute = path.join(root, rel);
62
+ let text;
63
+ try {
64
+ text = await fs.readFile(absolute, "utf8");
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ const lines = text.split(/\r?\n/);
70
+ for (let i = 0; i < lines.length; i += 1) {
71
+ const line = lines[i] ?? "";
72
+ for (const pattern of INJECTION_PATTERNS) {
73
+ if (!pattern.re.test(line))
74
+ continue;
75
+ findings.push({
76
+ id: `ctxsec_${createHash("sha1").update(`${rel}:${i}:${pattern.id}`).digest("hex").slice(0, 12)}`,
77
+ module: "context-security",
78
+ severity: pattern.severity,
79
+ title: pattern.title,
80
+ message: `Pattern ${pattern.id} in ${rel}:${i + 1}`,
81
+ recommendation: "Review and remove untrusted instructions; tighten agent policies",
82
+ confidence: 0.65,
83
+ evidence: [
84
+ {
85
+ kind: "inferred",
86
+ path: toPosixRelative(root, absolute),
87
+ line: i + 1,
88
+ detail: redactEvidenceDetail(line.slice(0, 200)),
89
+ },
90
+ ],
91
+ falsePositiveWarning: "Pattern match only — confirm intent before treating as an attack",
92
+ });
93
+ }
94
+ }
95
+ }
96
+ return findings.sort((a, b) => a.id.localeCompare(b.id));
97
+ }