@praneeth_54/agentdoctor 2.1.0 → 3.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.
Files changed (167) hide show
  1. package/CHANGELOG.md +86 -3
  2. package/README.md +490 -290
  3. package/dist/agent/chat/deterministic.d.ts +6 -0
  4. package/dist/agent/chat/deterministic.js +180 -0
  5. package/dist/agent/chat/service.js +8 -14
  6. package/dist/agent/context/retrieve.d.ts +2 -1
  7. package/dist/agent/context/retrieve.js +14 -1
  8. package/dist/agent/index.d.ts +2 -0
  9. package/dist/agent/index.js +1 -0
  10. package/dist/agent/loop.d.ts +2 -0
  11. package/dist/agent/loop.js +31 -0
  12. package/dist/agent/roles.d.ts +12 -0
  13. package/dist/agent/roles.js +138 -0
  14. package/dist/agent/runtime.d.ts +21 -5
  15. package/dist/agent/runtime.js +141 -28
  16. package/dist/agent/tools/execute.d.ts +2 -0
  17. package/dist/agent/tools/execute.js +49 -43
  18. package/dist/agent/tools/write.d.ts +1 -1
  19. package/dist/agent/tools/write.js +18 -4
  20. package/dist/ai/config.js +1 -0
  21. package/dist/ai/providers/adversarial-local.d.ts +7 -0
  22. package/dist/ai/providers/adversarial-local.js +146 -0
  23. package/dist/ai/redact.d.ts +1 -0
  24. package/dist/ai/redact.js +47 -7
  25. package/dist/ai/types.d.ts +1 -1
  26. package/dist/cli/commands/agent.d.ts +2 -0
  27. package/dist/cli/commands/agent.js +32 -11
  28. package/dist/cli/commands/architecture.js +7 -2
  29. package/dist/cli/commands/brain.js +7 -2
  30. package/dist/cli/commands/chat.js +15 -7
  31. package/dist/cli/commands/complete.js +13 -3
  32. package/dist/cli/commands/fix.js +6 -0
  33. package/dist/cli/commands/learn.js +7 -2
  34. package/dist/cli/commands/mcp.js +7 -3
  35. package/dist/cli/commands/platform.js +7 -2
  36. package/dist/cli/commands/policy-graph-run.js +7 -0
  37. package/dist/cli/commands/product.d.ts +19 -0
  38. package/dist/cli/commands/product.js +157 -0
  39. package/dist/cli/commands/scan.js +6 -0
  40. package/dist/cli/commands/start.d.ts +15 -0
  41. package/dist/cli/commands/start.js +86 -0
  42. package/dist/cli/commands/v2.js +61 -11
  43. package/dist/cli/commands/verify.js +6 -0
  44. package/dist/cli/program.js +141 -0
  45. package/dist/cli/safe-root.d.ts +17 -0
  46. package/dist/cli/safe-root.js +32 -0
  47. package/dist/constants.d.ts +1 -1
  48. package/dist/constants.js +1 -1
  49. package/dist/core/brain-cli/service.js +5 -2
  50. package/dist/core/monorepo/detect.js +10 -0
  51. package/dist/core/secrets/scan.js +9 -0
  52. package/dist/core/understanding/brain/storage/store.d.ts +4 -0
  53. package/dist/core/understanding/brain/storage/store.js +22 -2
  54. package/dist/dashboard/page.d.ts +2 -0
  55. package/dist/dashboard/page.js +53 -0
  56. package/dist/dashboard/server.js +117 -140
  57. package/dist/dashboard/ui/client.d.ts +2 -0
  58. package/dist/dashboard/ui/client.js +2 -0
  59. package/dist/dashboard/ui/styles.d.ts +2 -0
  60. package/dist/dashboard/ui/styles.js +138 -0
  61. package/dist/discovery/files.js +13 -0
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +2 -0
  64. package/dist/intelligence/graph/build.js +46 -38
  65. package/dist/intelligence/graph/incremental.d.ts +2 -0
  66. package/dist/intelligence/graph/incremental.js +9 -0
  67. package/dist/intelligence/resolve/imports.js +1 -1
  68. package/dist/languages/dart.d.ts +10 -0
  69. package/dist/languages/dart.js +99 -0
  70. package/dist/languages/go.d.ts +13 -7
  71. package/dist/languages/go.js +84 -24
  72. package/dist/languages/index.d.ts +5 -1
  73. package/dist/languages/index.js +13 -36
  74. package/dist/languages/java.d.ts +10 -0
  75. package/dist/languages/java.js +80 -0
  76. package/dist/languages/kotlin.d.ts +10 -0
  77. package/dist/languages/kotlin.js +85 -0
  78. package/dist/languages/rust.d.ts +10 -0
  79. package/dist/languages/rust.js +93 -0
  80. package/dist/languages/types.d.ts +4 -3
  81. package/dist/mcp/agent/registry.d.ts +1 -1
  82. package/dist/mcp/agent/registry.js +109 -23
  83. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  84. package/dist/mcp/intelligence/handlers.js +28 -0
  85. package/dist/mcp/intelligence/path-safety.d.ts +6 -1
  86. package/dist/mcp/intelligence/path-safety.js +55 -2
  87. package/dist/mcp/intelligence/registry.d.ts +1 -1
  88. package/dist/mcp/intelligence/registry.js +30 -1
  89. package/dist/platform/graph/build.js +9 -0
  90. package/dist/product/api/doctor.d.ts +14 -0
  91. package/dist/product/api/doctor.js +185 -0
  92. package/dist/product/api/openapi.d.ts +7 -0
  93. package/dist/product/api/openapi.js +122 -0
  94. package/dist/product/approval/model.d.ts +30 -0
  95. package/dist/product/approval/model.js +64 -0
  96. package/dist/product/approval/session.d.ts +49 -0
  97. package/dist/product/approval/session.js +134 -0
  98. package/dist/product/database/doctor.d.ts +30 -0
  99. package/dist/product/database/doctor.js +184 -0
  100. package/dist/product/decisions/ledger.d.ts +36 -0
  101. package/dist/product/decisions/ledger.js +141 -0
  102. package/dist/product/deps/analyze.d.ts +46 -0
  103. package/dist/product/deps/analyze.js +137 -0
  104. package/dist/product/deps/lockfiles.d.ts +25 -0
  105. package/dist/product/deps/lockfiles.js +200 -0
  106. package/dist/product/discovery/roots.d.ts +52 -0
  107. package/dist/product/discovery/roots.js +231 -0
  108. package/dist/product/dna/build.d.ts +50 -0
  109. package/dist/product/dna/build.js +255 -0
  110. package/dist/product/eval/lab.d.ts +17 -0
  111. package/dist/product/eval/lab.js +218 -0
  112. package/dist/product/events/doctor.d.ts +21 -0
  113. package/dist/product/events/doctor.js +147 -0
  114. package/dist/product/evidence-scan.d.ts +12 -0
  115. package/dist/product/evidence-scan.js +46 -0
  116. package/dist/product/evolution/timeline.d.ts +29 -0
  117. package/dist/product/evolution/timeline.js +123 -0
  118. package/dist/product/features/intelligence.d.ts +23 -0
  119. package/dist/product/features/intelligence.js +158 -0
  120. package/dist/product/forensic/mode.d.ts +25 -0
  121. package/dist/product/forensic/mode.js +70 -0
  122. package/dist/product/graph/enrich-languages.d.ts +20 -0
  123. package/dist/product/graph/enrich-languages.js +168 -0
  124. package/dist/product/health/code-health.d.ts +20 -0
  125. package/dist/product/health/code-health.js +149 -0
  126. package/dist/product/index.d.ts +72 -0
  127. package/dist/product/index.js +36 -0
  128. package/dist/product/ledger/change-ledger.d.ts +23 -0
  129. package/dist/product/ledger/change-ledger.js +64 -0
  130. package/dist/product/map/software-map.d.ts +17 -0
  131. package/dist/product/map/software-map.js +96 -0
  132. package/dist/product/memory/institutional.d.ts +16 -0
  133. package/dist/product/memory/institutional.js +92 -0
  134. package/dist/product/ops/incident.d.ts +20 -0
  135. package/dist/product/ops/incident.js +61 -0
  136. package/dist/product/ops/infra.d.ts +15 -0
  137. package/dist/product/ops/infra.js +112 -0
  138. package/dist/product/org/model.d.ts +34 -0
  139. package/dist/product/org/model.js +195 -0
  140. package/dist/product/privacy/doctor.d.ts +13 -0
  141. package/dist/product/privacy/doctor.js +90 -0
  142. package/dist/product/requirements/trace.d.ts +21 -0
  143. package/dist/product/requirements/trace.js +166 -0
  144. package/dist/product/search/index.d.ts +29 -0
  145. package/dist/product/search/index.js +116 -0
  146. package/dist/product/search/software-search.d.ts +19 -0
  147. package/dist/product/search/software-search.js +105 -0
  148. package/dist/product/security/doctor.d.ts +23 -0
  149. package/dist/product/security/doctor.js +124 -0
  150. package/dist/product/self/diagnose.d.ts +15 -0
  151. package/dist/product/self/diagnose.js +82 -0
  152. package/dist/product/techdebt/roadmap.d.ts +20 -0
  153. package/dist/product/techdebt/roadmap.js +118 -0
  154. package/dist/product/testbrain/analyze.d.ts +29 -0
  155. package/dist/product/testbrain/analyze.js +129 -0
  156. package/dist/product/truth.d.ts +12 -0
  157. package/dist/product/truth.js +30 -0
  158. package/dist/product/twin/digital-twin.d.ts +23 -0
  159. package/dist/product/twin/digital-twin.js +52 -0
  160. package/dist/product/twin/store.d.ts +21 -0
  161. package/dist/product/twin/store.js +78 -0
  162. package/dist/product/whatif/engine.d.ts +28 -0
  163. package/dist/product/whatif/engine.js +91 -0
  164. package/dist/project/ownership.d.ts +65 -0
  165. package/dist/project/ownership.js +169 -0
  166. package/dist/utils/fs.js +10 -2
  167. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { readTextFile } from "../../utils/fs.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { isLikelyRequirementsDoc, isMarkdownDoc } from "../evidence-scan.js";
7
+ const REQ_HEADING = /^(#{1,4})\s+(?:REQ[-\s]?(\d+)[:\s]+|Requirement\s+(\d+)[:\s]+|US[-\s]?(\d+)[:\s]+|User\s+Story\s+(\d+)[:\s]+)?(.+)$/i;
8
+ const AC_LINE = /^\s*[-*]\s*(?:AC|Acceptance Criteria)[:\s]+(.+)$/i;
9
+ const AC_SECTION = /^#{1,4}\s+Acceptance Criteria\s*$/i;
10
+ function slugId(sourcePath, line, title) {
11
+ const slug = title
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9]+/g, "-")
14
+ .replace(/^-|-$/g, "")
15
+ .slice(0, 40);
16
+ return `${sourcePath.replace(/[^\w./-]/g, "_")}:${line}:${slug || "req"}`;
17
+ }
18
+ function testFileMightCover(title, testPaths) {
19
+ const tokens = title
20
+ .toLowerCase()
21
+ .split(/\W+/)
22
+ .filter((t) => t.length > 3);
23
+ if (!tokens.length)
24
+ return false;
25
+ for (const tp of testPaths) {
26
+ const lower = tp.toLowerCase();
27
+ if (tokens.some((t) => lower.includes(t)))
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+ function isTestPath(relativePath) {
33
+ const p = relativePath.toLowerCase();
34
+ return (p.includes(".test.") ||
35
+ p.includes(".spec.") ||
36
+ p.startsWith("tests/") ||
37
+ p.startsWith("test/") ||
38
+ p.includes("__tests__/"));
39
+ }
40
+ function docEligible(relativePath, content) {
41
+ if (!isMarkdownDoc(relativePath))
42
+ return false;
43
+ if (isLikelyRequirementsDoc(relativePath))
44
+ return true;
45
+ if (/acceptance criteria/i.test(content))
46
+ return true;
47
+ if (/^#{1,4}\s+(?:REQ|Requirement|User Story)/im.test(content))
48
+ return true;
49
+ return false;
50
+ }
51
+ function parseRequirementsFromDoc(relativePath, content, testPaths) {
52
+ const lines = content.split(/\r?\n/);
53
+ const out = [];
54
+ let inAcSection = false;
55
+ let current = null;
56
+ const flush = () => {
57
+ if (!current)
58
+ return;
59
+ if (current.acceptanceCriteria.length === 0) {
60
+ current.truth = "PARTIAL";
61
+ }
62
+ current.missingTestHeuristic = !testFileMightCover(current.title, testPaths);
63
+ if (current.missingTestHeuristic) {
64
+ current.missingTestNote =
65
+ "No test file path matched requirement title tokens (heuristic only).";
66
+ }
67
+ out.push(current);
68
+ current = null;
69
+ inAcSection = false;
70
+ };
71
+ for (let i = 0; i < lines.length; i++) {
72
+ const line = lines[i] ?? "";
73
+ const lineNo = i + 1;
74
+ if (AC_SECTION.test(line.trim())) {
75
+ inAcSection = true;
76
+ continue;
77
+ }
78
+ const heading = REQ_HEADING.exec(line.trim());
79
+ if (heading) {
80
+ flush();
81
+ const reqNum = heading[2] ?? heading[3] ?? heading[4] ?? heading[5];
82
+ const titlePart = (heading[6] ?? "").trim();
83
+ const base = relativePath.split("/").pop()?.toLowerCase() ?? "";
84
+ const explicitCatalog = base === "requirements.md" || base === "user-stories.md";
85
+ if (!reqNum && !explicitCatalog)
86
+ continue;
87
+ const title = reqNum ? titlePart : titlePart || line.replace(/^#{1,4}\s+/, "").trim();
88
+ if (!title || title.length < 3)
89
+ continue;
90
+ const displayTitle = reqNum ? `REQ-${reqNum}: ${title}` : title;
91
+ current = {
92
+ id: slugId(relativePath, lineNo, displayTitle),
93
+ title: displayTitle,
94
+ sourcePath: relativePath,
95
+ sourceLine: lineNo,
96
+ acceptanceCriteria: [],
97
+ truth: "VERIFIED",
98
+ evidence: [{ path: relativePath, line: lineNo, excerpt: line.trim().slice(0, 120) }],
99
+ missingTestHeuristic: false,
100
+ };
101
+ inAcSection = false;
102
+ continue;
103
+ }
104
+ if (!current)
105
+ continue;
106
+ const acInline = AC_LINE.exec(line);
107
+ if (acInline?.[1]) {
108
+ current.acceptanceCriteria.push(acInline[1].trim());
109
+ current.evidence.push({
110
+ path: relativePath,
111
+ line: lineNo,
112
+ excerpt: line.trim().slice(0, 120),
113
+ });
114
+ continue;
115
+ }
116
+ if (inAcSection && /^\s*[-*]\s+/.test(line)) {
117
+ const item = line.replace(/^\s*[-*]\s+/, "").trim();
118
+ if (item) {
119
+ current.acceptanceCriteria.push(item);
120
+ current.evidence.push({
121
+ path: relativePath,
122
+ line: lineNo,
123
+ excerpt: item.slice(0, 120),
124
+ });
125
+ }
126
+ }
127
+ else if (inAcSection && line.trim() === "") {
128
+ continue;
129
+ }
130
+ else if (inAcSection && /^#{1,4}\s/.test(line)) {
131
+ inAcSection = false;
132
+ }
133
+ }
134
+ flush();
135
+ return out;
136
+ }
137
+ /**
138
+ * Scan repository docs for requirements / user stories with file evidence only.
139
+ */
140
+ export async function traceRequirements(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
141
+ const root = resolveRepoRoot(rootInput);
142
+ const detection = await detectProject(root, maxFileSizeBytes);
143
+ const limitations = [
144
+ "Requirements are extracted only from markdown headings and acceptance-criteria sections present in files.",
145
+ "Missing-test detection is a filename/token heuristic, not coverage analysis.",
146
+ ];
147
+ if (detection.diagnostics.length) {
148
+ limitations.push(...detection.diagnostics.slice(0, 3));
149
+ }
150
+ const testPaths = detection.discovery.files.map((f) => f.relativePath).filter(isTestPath);
151
+ const requirements = [];
152
+ for (const entry of detection.discovery.files) {
153
+ const rel = entry.relativePath.replace(/\\/g, "/");
154
+ const abs = path.join(root, rel);
155
+ const text = await readTextFile(abs, maxFileSizeBytes);
156
+ if (text === null)
157
+ continue;
158
+ if (!docEligible(rel, text))
159
+ continue;
160
+ requirements.push(...parseRequirementsFromDoc(rel, text, testPaths));
161
+ }
162
+ requirements.sort((a, b) => a.sourcePath === b.sourcePath
163
+ ? a.sourceLine - b.sourceLine
164
+ : a.sourcePath.localeCompare(b.sourcePath));
165
+ return { root, requirements, limitations };
166
+ }
@@ -0,0 +1,29 @@
1
+ export interface IndexDocument {
2
+ id: string;
3
+ path: string;
4
+ text: string;
5
+ }
6
+ export interface TfidfIndex {
7
+ documents: IndexDocument[];
8
+ idf: Map<string, number>;
9
+ docVectors: Map<string, Map<string, number>>;
10
+ limitations: string[];
11
+ }
12
+ export declare function buildIndex(documents: IndexDocument[]): TfidfIndex;
13
+ export declare function searchIndex(index: TfidfIndex, query: string, maxHits?: number): Array<{
14
+ document: IndexDocument;
15
+ score: number;
16
+ }>;
17
+ export declare function documentId(path: string, content: string): string;
18
+ export declare function searchHybrid(index: TfidfIndex, query: string, substringHits: Array<{
19
+ path: string;
20
+ line?: number;
21
+ excerpt: string;
22
+ score: number;
23
+ }>, maxHits?: number): Array<{
24
+ path: string;
25
+ line?: number;
26
+ excerpt: string;
27
+ score: number;
28
+ kind: "tfidf" | "keyword";
29
+ }>;
@@ -0,0 +1,116 @@
1
+ import { createHash } from "node:crypto";
2
+ const TOKEN_RE = /[a-zA-Z][a-zA-Z0-9_]{1,48}|[0-9]+/g;
3
+ function tokenize(text) {
4
+ const lower = text.toLowerCase();
5
+ const matches = lower.match(TOKEN_RE);
6
+ return matches ?? [];
7
+ }
8
+ function termFrequency(tokens) {
9
+ const tf = new Map();
10
+ for (const t of tokens) {
11
+ tf.set(t, (tf.get(t) ?? 0) + 1);
12
+ }
13
+ const max = Math.max(1, ...tf.values());
14
+ for (const [k, v] of tf) {
15
+ tf.set(k, v / max);
16
+ }
17
+ return tf;
18
+ }
19
+ export function buildIndex(documents) {
20
+ const limitations = [
21
+ "Local index uses keyword TF-IDF — not neural embeddings or semantic similarity.",
22
+ ];
23
+ const docTokens = new Map();
24
+ const df = new Map();
25
+ for (const doc of documents) {
26
+ const tokens = tokenize(doc.text);
27
+ docTokens.set(doc.id, tokens);
28
+ const unique = new Set(tokens);
29
+ for (const t of unique) {
30
+ df.set(t, (df.get(t) ?? 0) + 1);
31
+ }
32
+ }
33
+ const n = Math.max(1, documents.length);
34
+ const idf = new Map();
35
+ for (const [term, count] of df) {
36
+ idf.set(term, Math.log((1 + n) / (1 + count)) + 1);
37
+ }
38
+ const docVectors = new Map();
39
+ for (const doc of documents) {
40
+ const tf = termFrequency(docTokens.get(doc.id) ?? []);
41
+ const vec = new Map();
42
+ for (const [term, weight] of tf) {
43
+ const idfVal = idf.get(term) ?? 1;
44
+ vec.set(term, weight * idfVal);
45
+ }
46
+ docVectors.set(doc.id, vec);
47
+ }
48
+ return { documents, idf, docVectors, limitations };
49
+ }
50
+ function vectorNorm(vec) {
51
+ let sum = 0;
52
+ for (const v of vec.values())
53
+ sum += v * v;
54
+ return Math.sqrt(sum) || 1;
55
+ }
56
+ function cosineSimilarity(a, b) {
57
+ let dot = 0;
58
+ const smaller = a.size <= b.size ? a : b;
59
+ const other = a.size <= b.size ? b : a;
60
+ for (const [term, weight] of smaller) {
61
+ const ow = other.get(term);
62
+ if (ow !== undefined)
63
+ dot += weight * ow;
64
+ }
65
+ return dot / (vectorNorm(a) * vectorNorm(b));
66
+ }
67
+ export function searchIndex(index, query, maxHits = 40) {
68
+ const qTokens = tokenize(query);
69
+ if (!qTokens.length)
70
+ return [];
71
+ const qTf = termFrequency(qTokens);
72
+ const qVec = new Map();
73
+ for (const [term, weight] of qTf) {
74
+ qVec.set(term, weight * (index.idf.get(term) ?? 1));
75
+ }
76
+ const hits = [];
77
+ for (const doc of index.documents) {
78
+ const dVec = index.docVectors.get(doc.id);
79
+ if (!dVec)
80
+ continue;
81
+ const score = cosineSimilarity(qVec, dVec);
82
+ if (score > 0)
83
+ hits.push({ document: doc, score });
84
+ }
85
+ hits.sort((a, b) => b.score - a.score || a.document.path.localeCompare(b.document.path));
86
+ return hits.slice(0, maxHits);
87
+ }
88
+ export function documentId(path, content) {
89
+ return createHash("sha1").update(`${path}\0${content.length}`).digest("hex").slice(0, 16);
90
+ }
91
+ export function searchHybrid(index, query, substringHits, maxHits = 40) {
92
+ const tfidf = searchIndex(index, query, maxHits);
93
+ const merged = new Map();
94
+ for (const h of tfidf) {
95
+ const key = h.document.path;
96
+ merged.set(key, {
97
+ path: h.document.path,
98
+ excerpt: h.document.text.slice(0, 140),
99
+ score: h.score + 0.05,
100
+ kind: "tfidf",
101
+ });
102
+ }
103
+ for (const h of substringHits) {
104
+ const key = `${h.path}:${h.line ?? 0}`;
105
+ const prev = merged.get(key);
106
+ if (!prev || h.score > prev.score) {
107
+ merged.set(key, { ...h, kind: "keyword" });
108
+ }
109
+ else if (prev) {
110
+ merged.set(key, { ...prev, score: prev.score + h.score * 0.5 });
111
+ }
112
+ }
113
+ return [...merged.values()]
114
+ .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
115
+ .slice(0, maxHits);
116
+ }
@@ -0,0 +1,19 @@
1
+ import type { TruthLabel } from "../truth.js";
2
+ export interface SoftwareSearchHit {
3
+ kind: "filename" | "content" | "brain" | "tfidf";
4
+ path: string;
5
+ line?: number;
6
+ excerpt: string;
7
+ score: number;
8
+ truth: TruthLabel;
9
+ }
10
+ export interface SoftwareSearchReport {
11
+ root: string;
12
+ query: string;
13
+ hits: SoftwareSearchHit[];
14
+ limitations: string[];
15
+ }
16
+ export declare function searchSymbolsAndConcepts(rootInput: string, query: string, options?: {
17
+ maxHits?: number;
18
+ maxFiles?: number;
19
+ }): Promise<SoftwareSearchReport>;
@@ -0,0 +1,105 @@
1
+ import path from "node:path";
2
+ import { loadLatestBrain, searchBrain } from "../../core/brain-cli/service.js";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { readTextFile } from "../../utils/fs.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { buildIndex, documentId, searchHybrid } from "./index.js";
7
+ export async function searchSymbolsAndConcepts(rootInput, query, options) {
8
+ const root = resolveRepoRoot(rootInput);
9
+ const q = query.trim();
10
+ const maxHits = options?.maxHits ?? 40;
11
+ const maxFiles = options?.maxFiles ?? 300;
12
+ const limitations = [
13
+ "Search combines TF-IDF keyword index + brain claims + bounded filename/content scan.",
14
+ "TF-IDF is lexical — not neural embeddings or semantic code search.",
15
+ "Content substring hits are INFERRED.",
16
+ ];
17
+ if (!q) {
18
+ return { root, query: q, hits: [], limitations: [...limitations, "Empty query"] };
19
+ }
20
+ const hits = [];
21
+ const qLower = q.toLowerCase();
22
+ const brain = await loadLatestBrain(root);
23
+ if (brain) {
24
+ for (const bh of searchBrain(brain, q)) {
25
+ if (bh.text.includes(".private/") ||
26
+ bh.text.includes("AgentDoctorOS/") ||
27
+ bh.text.includes("oss-validation/")) {
28
+ continue;
29
+ }
30
+ hits.push({
31
+ kind: "brain",
32
+ path: ".agentdoctor/project-brain",
33
+ excerpt: bh.text,
34
+ score: bh.score + 0.2,
35
+ truth: "INFERRED",
36
+ });
37
+ }
38
+ }
39
+ else {
40
+ limitations.push("No project brain snapshot; brain search skipped.");
41
+ }
42
+ const detection = await detectProject(root);
43
+ let filesScanned = 0;
44
+ const indexDocs = [];
45
+ const substringHits = [];
46
+ for (const entry of detection.discovery.files) {
47
+ if (filesScanned >= maxFiles)
48
+ break;
49
+ const rel = entry.relativePath.replace(/\\/g, "/");
50
+ filesScanned += 1;
51
+ const base = path.basename(rel).toLowerCase();
52
+ if (base.includes(qLower)) {
53
+ hits.push({
54
+ kind: "filename",
55
+ path: rel,
56
+ excerpt: rel,
57
+ score: 1.1,
58
+ truth: "VERIFIED",
59
+ });
60
+ }
61
+ const text = await readTextFile(path.join(root, rel), 256 * 1024);
62
+ if (text === null)
63
+ continue;
64
+ indexDocs.push({
65
+ id: documentId(rel, text),
66
+ path: rel,
67
+ text: `${rel}\n${text.slice(0, 8_000)}`,
68
+ });
69
+ const idx = text.toLowerCase().indexOf(qLower);
70
+ if (idx >= 0) {
71
+ const line = text.slice(0, idx).split("\n").length;
72
+ const lineText = text.split(/\r?\n/)[line - 1]?.trim() ?? "";
73
+ substringHits.push({
74
+ path: rel,
75
+ line,
76
+ excerpt: lineText.slice(0, 140),
77
+ score: 1,
78
+ });
79
+ }
80
+ }
81
+ if (filesScanned >= maxFiles) {
82
+ limitations.push(`Content scan capped at ${maxFiles} files`);
83
+ }
84
+ if (indexDocs.length) {
85
+ const index = buildIndex(indexDocs);
86
+ limitations.push(...index.limitations);
87
+ for (const h of searchHybrid(index, q, substringHits, maxHits)) {
88
+ hits.push({
89
+ kind: h.kind === "tfidf" ? "tfidf" : "content",
90
+ path: h.path,
91
+ ...(h.line !== undefined ? { line: h.line } : {}),
92
+ excerpt: h.excerpt,
93
+ score: h.score,
94
+ truth: h.kind === "tfidf" ? "INFERRED" : "INFERRED",
95
+ });
96
+ }
97
+ }
98
+ hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
99
+ return {
100
+ root,
101
+ query: q,
102
+ hits: hits.slice(0, maxHits),
103
+ limitations,
104
+ };
105
+ }
@@ -0,0 +1,23 @@
1
+ import { type SecretFinding } from "../../core/secrets/scan.js";
2
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
3
+ export interface SecurityPatternFinding {
4
+ id: string;
5
+ title: string;
6
+ truth: TruthLabel;
7
+ evidence: ProductEvidence[];
8
+ }
9
+ export interface SecurityDoctorReport {
10
+ root: string;
11
+ /** Local heuristic rules only — not enterprise SAST. */
12
+ staticAnalysisLabel: "TECHNICAL_STATIC_ANALYSIS_SUPPORTED";
13
+ secretScan: {
14
+ enabled: boolean;
15
+ filesScanned: number;
16
+ findings: SecretFinding[];
17
+ limitations: string[];
18
+ };
19
+ authMarkers: SecurityPatternFinding[];
20
+ dangerousPatterns: SecurityPatternFinding[];
21
+ limitations: string[];
22
+ }
23
+ export declare function analyzeSecuritySurface(rootInput: string): Promise<SecurityDoctorReport>;
@@ -0,0 +1,124 @@
1
+ import { scanSecrets } from "../../core/secrets/scan.js";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ import { lineNumberAt, loadProjectSourceFiles } from "../evidence-scan.js";
4
+ const AUTH_MARKERS = [
5
+ {
6
+ id: "middleware-auth",
7
+ title: "Auth middleware marker",
8
+ re: /\b(?:auth|authenticate|authorize)\s*\(/i,
9
+ },
10
+ { id: "laravel-gate", title: "Laravel Gate/Policy marker", re: /\bGate::|\bPolicy\b|\b@can\b/i },
11
+ { id: "nestjs-guard", title: "NestJS guard/decorator marker", re: /@UseGuards\b|AuthGuard\b/i },
12
+ {
13
+ id: "rbac-role",
14
+ title: "RBAC role/permission marker",
15
+ re: /\b(?:hasPermission|hasRole|@PreAuthorize|@Roles)\b/i,
16
+ },
17
+ ];
18
+ const DANGEROUS_PATTERNS = [
19
+ { id: "eval-call", title: "Dynamic eval() (CWE-95)", re: /\beval\s*\(/g },
20
+ {
21
+ id: "child-process-exec-string",
22
+ title: "child_process exec with string literal (CWE-78)",
23
+ re: /\b(?:exec|execSync)\s*\(\s*['"`]/g,
24
+ },
25
+ {
26
+ id: "child-process-exec-concat",
27
+ title: "child_process exec with string concatenation (CWE-78)",
28
+ re: /\b(?:exec|execSync)\s*\(\s*[^)]*\+/g,
29
+ },
30
+ {
31
+ id: "innerhtml-assign",
32
+ title: "DOM innerHTML assignment (CWE-79)",
33
+ re: /\.innerHTML\s*=/g,
34
+ },
35
+ {
36
+ id: "path-join-user-input",
37
+ title: "Path join with request/user input pattern (CWE-22)",
38
+ re: /\bpath\.(?:join|resolve)\s*\([^)]*(?:req\.|request\.|params\.|query\.|user[A-Z_a-z]*|input)/g,
39
+ },
40
+ { id: "shell-exec", title: "Shell invocation helper", re: /\bshell\s*:\s*true\b/g },
41
+ ];
42
+ function redactExcerpt(line) {
43
+ return line
44
+ .trim()
45
+ .slice(0, 120)
46
+ .replace(/['"][^'"]{8,}['"]/g, "'[REDACTED]'");
47
+ }
48
+ export async function analyzeSecuritySurface(rootInput) {
49
+ const root = resolveRepoRoot(rootInput);
50
+ const limitations = [
51
+ "TECHNICAL STATIC ANALYSIS SUPPORTED locally (regex/heuristic rules with file evidence — not full SAST).",
52
+ "Security doctor uses heuristic pattern scans — not a substitute for SAST/DAST or manual review.",
53
+ "Secret values are never included; only redacted snippets and rule ids.",
54
+ ];
55
+ const secretScan = await scanSecrets({ root, enabled: true, maxFiles: 400 });
56
+ const { files } = await loadProjectSourceFiles(root, (rel) => /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go)$/i.test(rel));
57
+ const authMarkers = [];
58
+ const dangerousPatterns = [];
59
+ for (const file of files) {
60
+ for (const marker of AUTH_MARKERS) {
61
+ marker.re.lastIndex = 0;
62
+ if (marker.re.test(file.content)) {
63
+ const idx = file.content.search(marker.re);
64
+ authMarkers.push({
65
+ id: marker.id,
66
+ title: marker.title,
67
+ truth: "INFERRED",
68
+ evidence: [
69
+ {
70
+ path: file.relativePath,
71
+ ...(idx >= 0 ? { line: lineNumberAt(file.content, idx) } : {}),
72
+ excerpt: redactExcerpt(file.content.split(/\r?\n/)[Math.max(0, lineNumberAt(file.content, Math.max(idx, 0)) - 1)] ?? ""),
73
+ },
74
+ ],
75
+ });
76
+ break;
77
+ }
78
+ }
79
+ for (const pattern of DANGEROUS_PATTERNS) {
80
+ pattern.re.lastIndex = 0;
81
+ let match;
82
+ while ((match = pattern.re.exec(file.content)) !== null) {
83
+ const line = lineNumberAt(file.content, match.index);
84
+ const lineText = file.content.split(/\r?\n/)[line - 1] ?? "";
85
+ dangerousPatterns.push({
86
+ id: pattern.id,
87
+ title: pattern.title,
88
+ truth: "INFERRED",
89
+ evidence: [
90
+ {
91
+ path: file.relativePath,
92
+ line,
93
+ excerpt: redactExcerpt(lineText),
94
+ },
95
+ ],
96
+ });
97
+ if (dangerousPatterns.length >= 80)
98
+ break;
99
+ }
100
+ if (dangerousPatterns.length >= 80)
101
+ break;
102
+ }
103
+ if (dangerousPatterns.length >= 80) {
104
+ limitations.push("Dangerous pattern finding cap reached (80)");
105
+ break;
106
+ }
107
+ }
108
+ if (authMarkers.length === 0) {
109
+ limitations.push("No auth/permission file markers matched heuristics (absence is not proof).");
110
+ }
111
+ return {
112
+ root,
113
+ staticAnalysisLabel: "TECHNICAL_STATIC_ANALYSIS_SUPPORTED",
114
+ secretScan: {
115
+ enabled: secretScan.enabled,
116
+ filesScanned: secretScan.filesScanned,
117
+ findings: secretScan.findings,
118
+ limitations: secretScan.limitations,
119
+ },
120
+ authMarkers: authMarkers.slice(0, 40),
121
+ dangerousPatterns,
122
+ limitations,
123
+ };
124
+ }
@@ -0,0 +1,15 @@
1
+ import type { TruthLabel } from "../truth.js";
2
+ export interface SelfDiagnoseFinding {
3
+ id: string;
4
+ label: string;
5
+ ok: boolean;
6
+ truth: TruthLabel;
7
+ evidence: string;
8
+ }
9
+ export interface SelfDiagnoseReport {
10
+ root: string;
11
+ ok: boolean;
12
+ findings: SelfDiagnoseFinding[];
13
+ limitations: string[];
14
+ }
15
+ export declare function diagnoseAgentDoctorSelf(rootInput?: string): Promise<SelfDiagnoseReport>;
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { pathExists } from "../../utils/fs.js";
6
+ const REQUIRED_MODULES = [
7
+ "src/security/paths.ts",
8
+ "src/enforcement/runner.ts",
9
+ "src/assurance/change.ts",
10
+ "src/intelligence/graph/build.ts",
11
+ ];
12
+ export async function diagnoseAgentDoctorSelf(rootInput) {
13
+ const root = resolveRepoRoot(rootInput ?? process.cwd());
14
+ const limitations = [
15
+ "Self-check reports VERIFIED findings only when files/signals are present on disk.",
16
+ "Does not validate npm publish or CI status.",
17
+ ];
18
+ const findings = [];
19
+ try {
20
+ const detection = await detectProject(root);
21
+ const langs = detection.repository.languages ?? [];
22
+ findings.push({
23
+ id: "detect-project",
24
+ label: "detectProject completes",
25
+ ok: true,
26
+ truth: "VERIFIED",
27
+ evidence: `languages=${langs.join(",") || "none"}`,
28
+ });
29
+ }
30
+ catch (error) {
31
+ findings.push({
32
+ id: "detect-project",
33
+ label: "detectProject completes",
34
+ ok: false,
35
+ truth: "VERIFIED",
36
+ evidence: error instanceof Error ? error.message : String(error),
37
+ });
38
+ }
39
+ const testsDir = path.join(root, "tests");
40
+ const testsExists = await pathExists(testsDir);
41
+ findings.push({
42
+ id: "tests-dir",
43
+ label: "tests/ directory exists",
44
+ ok: testsExists,
45
+ truth: testsExists ? "VERIFIED" : "VERIFIED",
46
+ evidence: testsExists ? "tests/" : "missing tests/",
47
+ });
48
+ const pkgPath = path.join(root, "package.json");
49
+ let scriptsOk = false;
50
+ let scriptEvidence = "no package.json";
51
+ if (await pathExists(pkgPath)) {
52
+ try {
53
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
54
+ const scripts = pkg.scripts ?? {};
55
+ scriptsOk = typeof scripts.test === "string";
56
+ scriptEvidence = scriptsOk ? `test script present` : "test script missing";
57
+ }
58
+ catch {
59
+ scriptEvidence = "package.json unreadable";
60
+ }
61
+ }
62
+ findings.push({
63
+ id: "package-test-script",
64
+ label: "package.json defines test script",
65
+ ok: scriptsOk,
66
+ truth: "VERIFIED",
67
+ evidence: scriptEvidence,
68
+ });
69
+ for (const rel of REQUIRED_MODULES) {
70
+ const abs = path.join(root, rel);
71
+ const ok = await pathExists(abs);
72
+ findings.push({
73
+ id: `module-${rel.replace(/[^\w]/g, "-")}`,
74
+ label: `Required module ${rel}`,
75
+ ok,
76
+ truth: "VERIFIED",
77
+ evidence: ok ? "present" : "missing",
78
+ });
79
+ }
80
+ const ok = findings.every((f) => f.ok);
81
+ return { root, ok, findings, limitations };
82
+ }