@praneeth_54/agentdoctor 2.0.1 → 3.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 (192) hide show
  1. package/CHANGELOG.md +123 -1
  2. package/README.md +510 -260
  3. package/dist/agent/approvals.d.ts +24 -0
  4. package/dist/agent/approvals.js +64 -0
  5. package/dist/agent/chat/deterministic.d.ts +6 -0
  6. package/dist/agent/chat/deterministic.js +180 -0
  7. package/dist/agent/chat/index.d.ts +7 -0
  8. package/dist/agent/chat/index.js +5 -0
  9. package/dist/agent/chat/memory.d.ts +44 -0
  10. package/dist/agent/chat/memory.js +103 -0
  11. package/dist/agent/chat/project-summary.d.ts +16 -0
  12. package/dist/agent/chat/project-summary.js +70 -0
  13. package/dist/agent/chat/prompts.d.ts +6 -0
  14. package/dist/agent/chat/prompts.js +39 -0
  15. package/dist/agent/chat/response.d.ts +19 -0
  16. package/dist/agent/chat/response.js +109 -0
  17. package/dist/agent/chat/service.d.ts +42 -0
  18. package/dist/agent/chat/service.js +246 -0
  19. package/dist/agent/chat/types.d.ts +48 -0
  20. package/dist/agent/chat/types.js +1 -0
  21. package/dist/agent/context/retrieve.d.ts +21 -0
  22. package/dist/agent/context/retrieve.js +117 -0
  23. package/dist/agent/context/truth.d.ts +6 -0
  24. package/dist/agent/context/truth.js +19 -0
  25. package/dist/agent/context/types.d.ts +23 -0
  26. package/dist/agent/context/types.js +4 -0
  27. package/dist/agent/index.d.ts +28 -0
  28. package/dist/agent/index.js +15 -0
  29. package/dist/agent/loop.d.ts +53 -0
  30. package/dist/agent/loop.js +253 -0
  31. package/dist/agent/modes.d.ts +18 -0
  32. package/dist/agent/modes.js +102 -0
  33. package/dist/agent/plan.d.ts +30 -0
  34. package/dist/agent/plan.js +121 -0
  35. package/dist/agent/roles.d.ts +12 -0
  36. package/dist/agent/roles.js +138 -0
  37. package/dist/agent/runtime.d.ts +83 -0
  38. package/dist/agent/runtime.js +293 -0
  39. package/dist/agent/state.d.ts +30 -0
  40. package/dist/agent/state.js +95 -0
  41. package/dist/agent/student.d.ts +56 -0
  42. package/dist/agent/student.js +230 -0
  43. package/dist/agent/tools/execute.d.ts +20 -0
  44. package/dist/agent/tools/execute.js +365 -0
  45. package/dist/agent/tools/index.d.ts +6 -0
  46. package/dist/agent/tools/index.js +5 -0
  47. package/dist/agent/tools/registry.d.ts +7 -0
  48. package/dist/agent/tools/registry.js +212 -0
  49. package/dist/agent/tools/run.d.ts +24 -0
  50. package/dist/agent/tools/run.js +44 -0
  51. package/dist/agent/tools/types.d.ts +32 -0
  52. package/dist/agent/tools/types.js +10 -0
  53. package/dist/agent/tools/write.d.ts +24 -0
  54. package/dist/agent/tools/write.js +121 -0
  55. package/dist/agent/verify.d.ts +29 -0
  56. package/dist/agent/verify.js +210 -0
  57. package/dist/ai/config.d.ts +22 -0
  58. package/dist/ai/config.js +69 -0
  59. package/dist/ai/index.d.ts +17 -0
  60. package/dist/ai/index.js +52 -0
  61. package/dist/ai/providers/mock.d.ts +20 -0
  62. package/dist/ai/providers/mock.js +84 -0
  63. package/dist/ai/providers/none.d.ts +6 -0
  64. package/dist/ai/providers/none.js +23 -0
  65. package/dist/ai/providers/openai-compatible.d.ts +21 -0
  66. package/dist/ai/providers/openai-compatible.js +151 -0
  67. package/dist/ai/redact.d.ts +9 -0
  68. package/dist/ai/redact.js +61 -0
  69. package/dist/ai/types.d.ts +71 -0
  70. package/dist/ai/types.js +6 -0
  71. package/dist/cli/commands/agent.d.ts +29 -0
  72. package/dist/cli/commands/agent.js +164 -0
  73. package/dist/cli/commands/chat.d.ts +14 -0
  74. package/dist/cli/commands/chat.js +153 -0
  75. package/dist/cli/commands/learn.d.ts +12 -0
  76. package/dist/cli/commands/learn.js +107 -0
  77. package/dist/cli/commands/product.d.ts +19 -0
  78. package/dist/cli/commands/product.js +147 -0
  79. package/dist/cli/commands/start.d.ts +15 -0
  80. package/dist/cli/commands/start.js +80 -0
  81. package/dist/cli/program.js +233 -0
  82. package/dist/constants.d.ts +1 -1
  83. package/dist/constants.js +1 -1
  84. package/dist/dashboard/server.d.ts +6 -0
  85. package/dist/dashboard/server.js +333 -43
  86. package/dist/enforcement/runner.d.ts +5 -0
  87. package/dist/enforcement/runner.js +51 -4
  88. package/dist/index.d.ts +17 -0
  89. package/dist/index.js +10 -0
  90. package/dist/intelligence/graph/build.js +36 -8
  91. package/dist/intelligence/resolve/imports.js +1 -1
  92. package/dist/languages/dart.d.ts +10 -0
  93. package/dist/languages/dart.js +99 -0
  94. package/dist/languages/go.d.ts +13 -7
  95. package/dist/languages/go.js +91 -26
  96. package/dist/languages/index.d.ts +5 -1
  97. package/dist/languages/index.js +13 -36
  98. package/dist/languages/java.d.ts +10 -0
  99. package/dist/languages/java.js +80 -0
  100. package/dist/languages/kotlin.d.ts +10 -0
  101. package/dist/languages/kotlin.js +85 -0
  102. package/dist/languages/php.d.ts +1 -0
  103. package/dist/languages/php.js +11 -2
  104. package/dist/languages/python.d.ts +6 -2
  105. package/dist/languages/python.js +39 -9
  106. package/dist/languages/rust.d.ts +10 -0
  107. package/dist/languages/rust.js +93 -0
  108. package/dist/languages/types.d.ts +4 -3
  109. package/dist/mcp/agent/registry.d.ts +13 -0
  110. package/dist/mcp/agent/registry.js +320 -0
  111. package/dist/mcp/agentdoctor/server.js +8 -1
  112. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  113. package/dist/mcp/intelligence/handlers.js +28 -0
  114. package/dist/mcp/intelligence/registry.d.ts +1 -1
  115. package/dist/mcp/intelligence/registry.js +30 -1
  116. package/dist/product/api/doctor.d.ts +14 -0
  117. package/dist/product/api/doctor.js +185 -0
  118. package/dist/product/api/openapi.d.ts +7 -0
  119. package/dist/product/api/openapi.js +122 -0
  120. package/dist/product/approval/model.d.ts +30 -0
  121. package/dist/product/approval/model.js +64 -0
  122. package/dist/product/approval/session.d.ts +49 -0
  123. package/dist/product/approval/session.js +134 -0
  124. package/dist/product/database/doctor.d.ts +30 -0
  125. package/dist/product/database/doctor.js +184 -0
  126. package/dist/product/decisions/ledger.d.ts +24 -0
  127. package/dist/product/decisions/ledger.js +110 -0
  128. package/dist/product/deps/analyze.d.ts +46 -0
  129. package/dist/product/deps/analyze.js +137 -0
  130. package/dist/product/deps/lockfiles.d.ts +25 -0
  131. package/dist/product/deps/lockfiles.js +200 -0
  132. package/dist/product/discovery/roots.d.ts +42 -0
  133. package/dist/product/discovery/roots.js +215 -0
  134. package/dist/product/dna/build.d.ts +50 -0
  135. package/dist/product/dna/build.js +255 -0
  136. package/dist/product/eval/lab.d.ts +17 -0
  137. package/dist/product/eval/lab.js +218 -0
  138. package/dist/product/events/doctor.d.ts +21 -0
  139. package/dist/product/events/doctor.js +147 -0
  140. package/dist/product/evidence-scan.d.ts +12 -0
  141. package/dist/product/evidence-scan.js +46 -0
  142. package/dist/product/evolution/timeline.d.ts +29 -0
  143. package/dist/product/evolution/timeline.js +123 -0
  144. package/dist/product/features/intelligence.d.ts +23 -0
  145. package/dist/product/features/intelligence.js +158 -0
  146. package/dist/product/forensic/mode.d.ts +25 -0
  147. package/dist/product/forensic/mode.js +70 -0
  148. package/dist/product/graph/enrich-languages.d.ts +20 -0
  149. package/dist/product/graph/enrich-languages.js +193 -0
  150. package/dist/product/health/code-health.d.ts +20 -0
  151. package/dist/product/health/code-health.js +149 -0
  152. package/dist/product/index.d.ts +69 -0
  153. package/dist/product/index.js +35 -0
  154. package/dist/product/ledger/change-ledger.d.ts +23 -0
  155. package/dist/product/ledger/change-ledger.js +64 -0
  156. package/dist/product/map/software-map.d.ts +17 -0
  157. package/dist/product/map/software-map.js +95 -0
  158. package/dist/product/memory/institutional.d.ts +16 -0
  159. package/dist/product/memory/institutional.js +87 -0
  160. package/dist/product/ops/incident.d.ts +20 -0
  161. package/dist/product/ops/incident.js +61 -0
  162. package/dist/product/ops/infra.d.ts +15 -0
  163. package/dist/product/ops/infra.js +112 -0
  164. package/dist/product/org/model.d.ts +34 -0
  165. package/dist/product/org/model.js +195 -0
  166. package/dist/product/privacy/doctor.d.ts +13 -0
  167. package/dist/product/privacy/doctor.js +90 -0
  168. package/dist/product/requirements/trace.d.ts +21 -0
  169. package/dist/product/requirements/trace.js +166 -0
  170. package/dist/product/search/index.d.ts +29 -0
  171. package/dist/product/search/index.js +116 -0
  172. package/dist/product/search/software-search.d.ts +19 -0
  173. package/dist/product/search/software-search.js +100 -0
  174. package/dist/product/security/doctor.d.ts +23 -0
  175. package/dist/product/security/doctor.js +124 -0
  176. package/dist/product/self/diagnose.d.ts +15 -0
  177. package/dist/product/self/diagnose.js +82 -0
  178. package/dist/product/techdebt/roadmap.d.ts +20 -0
  179. package/dist/product/techdebt/roadmap.js +118 -0
  180. package/dist/product/testbrain/analyze.d.ts +29 -0
  181. package/dist/product/testbrain/analyze.js +129 -0
  182. package/dist/product/truth.d.ts +12 -0
  183. package/dist/product/truth.js +30 -0
  184. package/dist/product/twin/digital-twin.d.ts +23 -0
  185. package/dist/product/twin/digital-twin.js +52 -0
  186. package/dist/product/twin/store.d.ts +21 -0
  187. package/dist/product/twin/store.js +70 -0
  188. package/dist/product/whatif/engine.d.ts +28 -0
  189. package/dist/product/whatif/engine.js +70 -0
  190. package/dist/security/paths.js +43 -15
  191. package/dist/utils/fs.js +10 -2
  192. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface MemoryHit {
3
+ source: "brain" | "decisions" | "change-ledger" | "dna";
4
+ id: string;
5
+ title: string;
6
+ excerpt: string;
7
+ truth: TruthLabel;
8
+ evidence: ProductEvidence[];
9
+ }
10
+ export interface InstitutionalMemoryQueryResult {
11
+ root: string;
12
+ query: string;
13
+ hits: MemoryHit[];
14
+ limitations: string[];
15
+ }
16
+ export declare function queryMemory(rootInput: string, query: string): Promise<InstitutionalMemoryQueryResult>;
@@ -0,0 +1,87 @@
1
+ import { loadLatestBrain, searchBrain } from "../../core/brain-cli/service.js";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ import { buildProjectDna } from "../dna/build.js";
4
+ import { loadDecisionLedger } from "../decisions/ledger.js";
5
+ import { readChangeLedger } from "../ledger/change-ledger.js";
6
+ export async function queryMemory(rootInput, query) {
7
+ const root = resolveRepoRoot(rootInput);
8
+ const q = query.trim();
9
+ const limitations = [
10
+ "Institutional memory merges repo-local Brain, ADR/decision ledger, change ledger, and DNA — not a vector DB.",
11
+ "Ranking is substring match with simple scoring — not semantic search.",
12
+ "Cross-repo memory is not supported; query is scoped to the resolved catalog root.",
13
+ ];
14
+ if (!q) {
15
+ return { root, query: q, hits: [], limitations: [...limitations, "Empty query — no hits."] };
16
+ }
17
+ const hits = [];
18
+ const qLower = q.toLowerCase();
19
+ const brain = await loadLatestBrain(root);
20
+ if (brain) {
21
+ for (const bh of searchBrain(brain, q).slice(0, 15)) {
22
+ hits.push({
23
+ source: "brain",
24
+ id: bh.id,
25
+ title: bh.kind,
26
+ excerpt: bh.text.slice(0, 280),
27
+ truth: "VERIFIED",
28
+ evidence: [{ path: ".agentdoctor/project-brain", excerpt: bh.text.slice(0, 120) }],
29
+ });
30
+ }
31
+ }
32
+ else {
33
+ limitations.push("No Project Brain snapshot — brain hits skipped.");
34
+ }
35
+ const decisions = await loadDecisionLedger(root);
36
+ for (const d of decisions.decisions) {
37
+ const hay = `${d.title} ${d.bodyExcerpt ?? ""}`.toLowerCase();
38
+ if (!hay.includes(qLower))
39
+ continue;
40
+ hits.push({
41
+ source: "decisions",
42
+ id: d.id,
43
+ title: d.title,
44
+ excerpt: (d.bodyExcerpt ?? d.title).slice(0, 280),
45
+ truth: d.truth,
46
+ evidence: d.evidence,
47
+ });
48
+ }
49
+ const changeLedger = await readChangeLedger(root);
50
+ for (const entry of changeLedger.entries) {
51
+ const hay = `${entry.task ?? ""} ${entry.plan ?? ""} ${entry.note ?? ""} ${(entry.files ?? []).join(" ")}`.toLowerCase();
52
+ if (!hay.includes(qLower))
53
+ continue;
54
+ hits.push({
55
+ source: "change-ledger",
56
+ id: entry.id,
57
+ title: entry.task ?? entry.id,
58
+ excerpt: (entry.note ?? entry.plan ?? entry.task ?? "").slice(0, 280),
59
+ truth: "VERIFIED",
60
+ evidence: [{ path: ".agentdoctor/change-ledger/entries.jsonl", excerpt: entry.id }],
61
+ });
62
+ }
63
+ const dna = await buildProjectDna(root);
64
+ const dnaHay = JSON.stringify({
65
+ name: dna.name,
66
+ frameworks: dna.frameworks,
67
+ languages: dna.languages,
68
+ services: dna.services,
69
+ }).toLowerCase();
70
+ if (dnaHay.includes(qLower)) {
71
+ hits.push({
72
+ source: "dna",
73
+ id: dna.fingerprint,
74
+ title: `Project DNA: ${dna.name}`,
75
+ excerpt: `Languages: ${dna.languages.join(", ")}; frameworks: ${dna.frameworks.join(", ")}`,
76
+ truth: dna.truth,
77
+ evidence: [{ path: ".agentdoctor/dna/project-dna.json", excerpt: dna.fingerprint }],
78
+ });
79
+ }
80
+ hits.sort((a, b) => b.excerpt.length - a.excerpt.length);
81
+ return {
82
+ root,
83
+ query: q,
84
+ hits: hits.slice(0, 40),
85
+ limitations,
86
+ };
87
+ }
@@ -0,0 +1,20 @@
1
+ import type { TruthLabel } from "../truth.js";
2
+ export interface IncidentTimelineItem {
3
+ id: string;
4
+ hypothesis: string;
5
+ truth: TruthLabel;
6
+ relatedPaths: string[];
7
+ method: string;
8
+ }
9
+ export interface IncidentReport {
10
+ root: string;
11
+ timeline: IncidentTimelineItem[];
12
+ limitations: string[];
13
+ }
14
+ export declare function buildIncidentHypotheses(rootInput: string, options?: {
15
+ changedFiles?: string[];
16
+ gitHotspots?: Array<{
17
+ path: string;
18
+ commits: number;
19
+ }>;
20
+ }): Promise<IncidentReport>;
@@ -0,0 +1,61 @@
1
+ import { analyzeGitIntelligence } from "../../intelligence/git/analyze.js";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ function normalizePaths(paths) {
4
+ return [...new Set(paths.map((p) => p.replace(/\\/g, "/")))].sort();
5
+ }
6
+ export async function buildIncidentHypotheses(rootInput, options) {
7
+ const root = resolveRepoRoot(rootInput);
8
+ const limitations = [
9
+ "Timeline items are hypotheses for investigation, not verified incident root cause.",
10
+ "VERIFIED items require explicit changedFiles input or git hotspot file paths.",
11
+ ];
12
+ const changed = normalizePaths(options?.changedFiles ?? []);
13
+ const timeline = [];
14
+ if (changed.length) {
15
+ timeline.push({
16
+ id: "changed-files",
17
+ hypothesis: "Recent changes concentrated in supplied changedFiles list",
18
+ truth: "VERIFIED",
19
+ relatedPaths: changed,
20
+ method: "caller-supplied changedFiles",
21
+ });
22
+ }
23
+ let hotspots = options?.gitHotspots;
24
+ if (!hotspots) {
25
+ const git = await analyzeGitIntelligence(root);
26
+ if (git.gitAvailable) {
27
+ hotspots = git.hotspots.map((h) => ({ path: h.path, commits: h.commits }));
28
+ }
29
+ else {
30
+ limitations.push("Git unavailable; hotspot hypotheses skipped.");
31
+ }
32
+ }
33
+ if (hotspots?.length) {
34
+ const top = [...hotspots].sort((a, b) => b.commits - a.commits).slice(0, 5);
35
+ for (const h of top) {
36
+ const overlapsChange = changed.some((c) => c === h.path || h.path.endsWith(c) || c.endsWith(h.path));
37
+ timeline.push({
38
+ id: `hotspot-${h.path.replace(/[^\w]/g, "_")}`,
39
+ hypothesis: "Frequent historical churn (git hotspot) may relate to instability",
40
+ truth: overlapsChange ? "VERIFIED" : "INFERRED",
41
+ relatedPaths: [h.path.replace(/\\/g, "/")],
42
+ method: `git log file appearances (commits=${h.commits})`,
43
+ });
44
+ }
45
+ }
46
+ if (changed.length && hotspots?.length) {
47
+ const hotSet = new Set(hotspots.map((h) => h.path.replace(/\\/g, "/")));
48
+ const overlap = changed.filter((c) => hotSet.has(c));
49
+ if (overlap.length) {
50
+ timeline.push({
51
+ id: "change-hotspot-overlap",
52
+ hypothesis: "Changed files overlap historical hotspots",
53
+ truth: "VERIFIED",
54
+ relatedPaths: overlap,
55
+ method: "intersection(changedFiles, gitHotspots)",
56
+ });
57
+ }
58
+ }
59
+ timeline.sort((a, b) => a.id.localeCompare(b.id));
60
+ return { root, timeline, limitations };
61
+ }
@@ -0,0 +1,15 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface InfraArtifact {
3
+ kind: "docker" | "kubernetes" | "terraform" | "ci" | "compose-service" | "unknown";
4
+ path: string;
5
+ label: string;
6
+ truth: TruthLabel;
7
+ evidence: ProductEvidence[];
8
+ }
9
+ export interface InfraReport {
10
+ root: string;
11
+ artifacts: InfraArtifact[];
12
+ services: string[];
13
+ limitations: string[];
14
+ }
15
+ export declare function analyzeInfra(rootInput: string, maxFileSizeBytes?: number): Promise<InfraReport>;
@@ -0,0 +1,112 @@
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
+ function classifyPath(relativePath) {
7
+ const norm = relativePath.replace(/\\/g, "/");
8
+ const base = norm.split("/").pop()?.toLowerCase() ?? "";
9
+ if (base === "dockerfile" || base.startsWith("dockerfile.")) {
10
+ return {
11
+ kind: "docker",
12
+ path: norm,
13
+ label: "Dockerfile",
14
+ truth: "VERIFIED",
15
+ evidence: [{ path: norm }],
16
+ };
17
+ }
18
+ if (base === "docker-compose.yml" || base === "docker-compose.yaml" || base === "compose.yml") {
19
+ return {
20
+ kind: "docker",
21
+ path: norm,
22
+ label: "Docker Compose",
23
+ truth: "VERIFIED",
24
+ evidence: [{ path: norm }],
25
+ };
26
+ }
27
+ if (norm.includes(".github/workflows/") && (base.endsWith(".yml") || base.endsWith(".yaml"))) {
28
+ return {
29
+ kind: "ci",
30
+ path: norm,
31
+ label: "GitHub Actions workflow",
32
+ truth: "VERIFIED",
33
+ evidence: [{ path: norm }],
34
+ };
35
+ }
36
+ if (base.endsWith(".tf")) {
37
+ return {
38
+ kind: "terraform",
39
+ path: norm,
40
+ label: "Terraform",
41
+ truth: "VERIFIED",
42
+ evidence: [{ path: norm }],
43
+ };
44
+ }
45
+ if (norm.includes("kubernetes/") || base.includes("deployment") || base.includes("service")) {
46
+ if (base.endsWith(".yaml") || base.endsWith(".yml")) {
47
+ return {
48
+ kind: "kubernetes",
49
+ path: norm,
50
+ label: "Kubernetes manifest (heuristic)",
51
+ truth: "INFERRED",
52
+ evidence: [{ path: norm }],
53
+ };
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ function parseComposeServices(content, relativePath) {
59
+ const services = [];
60
+ let inServices = false;
61
+ for (const line of content.split(/\r?\n/)) {
62
+ const trimmed = line.trim();
63
+ if (/^services:\s*$/i.test(trimmed)) {
64
+ inServices = true;
65
+ continue;
66
+ }
67
+ if (inServices) {
68
+ if (/^[a-zA-Z0-9_-]+:\s*$/.test(trimmed) && !trimmed.startsWith("version:")) {
69
+ const name = trimmed.replace(":", "");
70
+ if (name !== "services" && !name.includes(" "))
71
+ services.push(name);
72
+ }
73
+ if (/^[a-zA-Z_]+:/.test(trimmed) &&
74
+ !line.startsWith(" ") &&
75
+ !line.startsWith("\t") &&
76
+ trimmed !== "services:") {
77
+ if (!trimmed.startsWith("services:"))
78
+ inServices = false;
79
+ }
80
+ }
81
+ }
82
+ return services.map((s) => `${s}@${relativePath}`);
83
+ }
84
+ export async function analyzeInfra(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
85
+ const root = resolveRepoRoot(rootInput);
86
+ const detection = await detectProject(root, maxFileSizeBytes);
87
+ const limitations = [
88
+ "Compose service names use simple line heuristics, not a full YAML parser.",
89
+ "Kubernetes detection is path/name heuristic.",
90
+ ];
91
+ const artifacts = [];
92
+ const services = [];
93
+ for (const entry of detection.discovery.files) {
94
+ const rel = entry.relativePath.replace(/\\/g, "/");
95
+ const artifact = classifyPath(rel);
96
+ if (artifact)
97
+ artifacts.push(artifact);
98
+ const base = rel.split("/").pop()?.toLowerCase() ?? "";
99
+ if (base === "docker-compose.yml" || base === "docker-compose.yaml" || base === "compose.yml") {
100
+ const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
101
+ if (text)
102
+ services.push(...parseComposeServices(text, rel));
103
+ }
104
+ }
105
+ artifacts.sort((a, b) => a.path.localeCompare(b.path));
106
+ return {
107
+ root,
108
+ artifacts,
109
+ services: [...new Set(services)].sort(),
110
+ limitations,
111
+ };
112
+ }
@@ -0,0 +1,34 @@
1
+ import type { TruthLabel } from "../truth.js";
2
+ export interface OrgEntityRef {
3
+ id: string;
4
+ name: string;
5
+ description?: string;
6
+ }
7
+ export interface OrgProjectLink {
8
+ id: string;
9
+ name: string;
10
+ /** Absolute or repo-relative root path for this project within the workspace catalog */
11
+ rootPath: string;
12
+ teamIds: string[];
13
+ serviceIds: string[];
14
+ ownerIds: string[];
15
+ }
16
+ export interface OrganizationModel {
17
+ schemaVersion: 1;
18
+ updatedAt: string;
19
+ /** Catalog root this model was loaded from (not shared across repos automatically) */
20
+ catalogRoot: string;
21
+ teams: OrgEntityRef[];
22
+ projects: OrgProjectLink[];
23
+ services: OrgEntityRef[];
24
+ owners: OrgEntityRef[];
25
+ truth: TruthLabel;
26
+ limitations: string[];
27
+ }
28
+ export declare function loadOrganizationModel(rootInput: string): Promise<OrganizationModel>;
29
+ export declare function saveOrganizationModel(rootInput: string, model: Omit<OrganizationModel, "schemaVersion" | "updatedAt" | "catalogRoot" | "truth" | "limitations">): Promise<OrganizationModel>;
30
+ /** Register or update a project link under the catalog root (path must stay within catalogRoot). */
31
+ export declare function linkProjectToOrgModel(rootInput: string, link: Omit<OrgProjectLink, "rootPath"> & {
32
+ rootPath?: string;
33
+ }): Promise<OrganizationModel>;
34
+ export declare function resolveLinkedProjectRoot(model: OrganizationModel, projectId: string): string | null;
@@ -0,0 +1,195 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { redactSecrets } from "../../platform/security/redact.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
6
+ import { pathExists, readJsonFile } from "../../utils/fs.js";
7
+ const FORBIDDEN_KEY = /^(secret|token|password|api[_-]?key|credential|private[_-]?key)$/i;
8
+ function modelPath(root) {
9
+ return path.join(resolveRepoRoot(root), ".agentdoctor", "org", "model.json");
10
+ }
11
+ function sanitizeString(value) {
12
+ return redactSecrets(value).text.slice(0, 2000);
13
+ }
14
+ function sanitizeEntityRef(raw) {
15
+ if (!raw || typeof raw !== "object")
16
+ return null;
17
+ const o = raw;
18
+ for (const key of Object.keys(o)) {
19
+ if (FORBIDDEN_KEY.test(key))
20
+ return null;
21
+ }
22
+ const id = typeof o.id === "string" ? sanitizeString(o.id.trim()) : "";
23
+ const name = typeof o.name === "string" ? sanitizeString(o.name.trim()) : "";
24
+ if (!id || !name)
25
+ return null;
26
+ const description = typeof o.description === "string" ? sanitizeString(o.description.trim()) : undefined;
27
+ return {
28
+ id,
29
+ name,
30
+ ...(description !== undefined && description.length > 0 ? { description } : {}),
31
+ };
32
+ }
33
+ function sanitizeProject(raw, catalogRoot) {
34
+ if (!raw || typeof raw !== "object")
35
+ return null;
36
+ const o = raw;
37
+ for (const key of Object.keys(o)) {
38
+ if (FORBIDDEN_KEY.test(key))
39
+ return null;
40
+ }
41
+ const id = typeof o.id === "string" ? sanitizeString(o.id.trim()) : "";
42
+ const name = typeof o.name === "string" ? sanitizeString(o.name.trim()) : "";
43
+ const rootPathRaw = typeof o.rootPath === "string" ? o.rootPath.trim() : catalogRoot;
44
+ if (!id || !name)
45
+ return null;
46
+ const resolved = path.isAbsolute(rootPathRaw)
47
+ ? path.normalize(rootPathRaw)
48
+ : path.normalize(path.join(catalogRoot, rootPathRaw));
49
+ if (!resolved.startsWith(catalogRoot)) {
50
+ return null;
51
+ }
52
+ const teamIds = Array.isArray(o.teamIds)
53
+ ? o.teamIds.filter((x) => typeof x === "string").map((x) => sanitizeString(x))
54
+ : [];
55
+ const serviceIds = Array.isArray(o.serviceIds)
56
+ ? o.serviceIds.filter((x) => typeof x === "string").map((x) => sanitizeString(x))
57
+ : [];
58
+ const ownerIds = Array.isArray(o.ownerIds)
59
+ ? o.ownerIds.filter((x) => typeof x === "string").map((x) => sanitizeString(x))
60
+ : [];
61
+ return { id, name, rootPath: resolved, teamIds, serviceIds, ownerIds };
62
+ }
63
+ function emptyModel(catalogRoot) {
64
+ return {
65
+ schemaVersion: 1,
66
+ updatedAt: new Date().toISOString(),
67
+ catalogRoot,
68
+ teams: [],
69
+ projects: [],
70
+ services: [],
71
+ owners: [],
72
+ truth: "VERIFIED",
73
+ limitations: [
74
+ "Org model is repo-local metadata only — not synced across machines or tenants.",
75
+ "Secret-like fields are rejected on load/save; values are redacted when persisted.",
76
+ "Linking a project root does not grant cross-repo file access — workspace rules still apply.",
77
+ ],
78
+ };
79
+ }
80
+ export async function loadOrganizationModel(rootInput) {
81
+ const catalogRoot = resolveRepoRoot(rootInput);
82
+ const file = modelPath(catalogRoot);
83
+ if (!(await pathExists(file))) {
84
+ return emptyModel(catalogRoot);
85
+ }
86
+ const parsed = await readJsonFile(file, DEFAULT_MAX_FILE_SIZE_BYTES);
87
+ if (!parsed.ok || typeof parsed.data !== "object" || parsed.data === null) {
88
+ return {
89
+ ...emptyModel(catalogRoot),
90
+ truth: "PARTIAL",
91
+ limitations: [
92
+ ...emptyModel(catalogRoot).limitations,
93
+ "Corrupt org model file — using empty model.",
94
+ ],
95
+ };
96
+ }
97
+ const o = parsed.data;
98
+ const teams = [];
99
+ const services = [];
100
+ const owners = [];
101
+ const projects = [];
102
+ if (Array.isArray(o.teams)) {
103
+ for (const item of o.teams) {
104
+ const ref = sanitizeEntityRef(item);
105
+ if (ref)
106
+ teams.push(ref);
107
+ }
108
+ }
109
+ if (Array.isArray(o.services)) {
110
+ for (const item of o.services) {
111
+ const ref = sanitizeEntityRef(item);
112
+ if (ref)
113
+ services.push(ref);
114
+ }
115
+ }
116
+ if (Array.isArray(o.owners)) {
117
+ for (const item of o.owners) {
118
+ const ref = sanitizeEntityRef(item);
119
+ if (ref)
120
+ owners.push(ref);
121
+ }
122
+ }
123
+ if (Array.isArray(o.projects)) {
124
+ for (const item of o.projects) {
125
+ const link = sanitizeProject(item, catalogRoot);
126
+ if (link)
127
+ projects.push(link);
128
+ }
129
+ }
130
+ return {
131
+ schemaVersion: 1,
132
+ updatedAt: typeof o.updatedAt === "string" && o.updatedAt.trim()
133
+ ? o.updatedAt.trim()
134
+ : new Date().toISOString(),
135
+ catalogRoot,
136
+ teams,
137
+ projects,
138
+ services,
139
+ owners,
140
+ truth: "VERIFIED",
141
+ limitations: emptyModel(catalogRoot).limitations,
142
+ };
143
+ }
144
+ export async function saveOrganizationModel(rootInput, model) {
145
+ const catalogRoot = resolveRepoRoot(rootInput);
146
+ const teams = model.teams
147
+ .map((t) => sanitizeEntityRef(t))
148
+ .filter((t) => t !== null);
149
+ const services = model.services
150
+ .map((s) => sanitizeEntityRef(s))
151
+ .filter((s) => s !== null);
152
+ const owners = model.owners
153
+ .map((o) => sanitizeEntityRef(o))
154
+ .filter((o) => o !== null);
155
+ const projects = model.projects
156
+ .map((p) => sanitizeProject(p, catalogRoot))
157
+ .filter((p) => p !== null);
158
+ const record = {
159
+ ...emptyModel(catalogRoot),
160
+ teams,
161
+ services,
162
+ owners,
163
+ projects,
164
+ updatedAt: new Date().toISOString(),
165
+ };
166
+ const dir = path.dirname(modelPath(catalogRoot));
167
+ await fs.mkdir(dir, { recursive: true });
168
+ await fs.writeFile(modelPath(catalogRoot), `${JSON.stringify(record, null, 2)}\n`, {
169
+ encoding: "utf8",
170
+ mode: 0o600,
171
+ });
172
+ return record;
173
+ }
174
+ /** Register or update a project link under the catalog root (path must stay within catalogRoot). */
175
+ export async function linkProjectToOrgModel(rootInput, link) {
176
+ const catalogRoot = resolveRepoRoot(rootInput);
177
+ const current = await loadOrganizationModel(catalogRoot);
178
+ const rootPath = link.rootPath ?? catalogRoot;
179
+ const sanitized = sanitizeProject({ ...link, rootPath }, catalogRoot);
180
+ if (!sanitized) {
181
+ throw new Error("Invalid project link — path escape or forbidden fields");
182
+ }
183
+ const projects = current.projects.filter((p) => p.id !== sanitized.id);
184
+ projects.push(sanitized);
185
+ return saveOrganizationModel(catalogRoot, {
186
+ teams: current.teams,
187
+ services: current.services,
188
+ owners: current.owners,
189
+ projects,
190
+ });
191
+ }
192
+ export function resolveLinkedProjectRoot(model, projectId) {
193
+ const hit = model.projects.find((p) => p.id === projectId);
194
+ return hit?.rootPath ?? null;
195
+ }
@@ -0,0 +1,13 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface PrivacyFinding {
3
+ id: string;
4
+ label: string;
5
+ truth: TruthLabel;
6
+ evidence: ProductEvidence[];
7
+ }
8
+ export interface PrivacyDoctorReport {
9
+ root: string;
10
+ findings: PrivacyFinding[];
11
+ limitations: string[];
12
+ }
13
+ export declare function analyzePrivacySurface(rootInput: string): Promise<PrivacyDoctorReport>;
@@ -0,0 +1,90 @@
1
+ import { loadLatestBrain, searchBrain } from "../../core/brain-cli/service.js";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ import { lineNumberAt, loadProjectSourceFiles } from "../evidence-scan.js";
4
+ const PII_PATTERNS = [
5
+ {
6
+ id: "email-like",
7
+ label: "Email-like string",
8
+ re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
9
+ },
10
+ {
11
+ id: "ssn-like",
12
+ label: "SSN-like digit groups",
13
+ re: /\b\d{3}-\d{2}-\d{4}\b/g,
14
+ },
15
+ {
16
+ id: "credit-card-like",
17
+ label: "Credit-card-like digit groups",
18
+ re: /\b(?:\d[ -]*?){13,19}\b/g,
19
+ },
20
+ ];
21
+ function redactPiiExcerpt(line, match) {
22
+ const idx = line.indexOf(match);
23
+ if (idx < 0)
24
+ return "[REDACTED]";
25
+ const before = line.slice(0, idx).slice(-24);
26
+ const after = line.slice(idx + match.length).slice(0, 12);
27
+ return `${before}[REDACTED]${after}`.trim().slice(0, 120);
28
+ }
29
+ export async function analyzePrivacySurface(rootInput) {
30
+ const root = resolveRepoRoot(rootInput);
31
+ const limitations = [
32
+ "Privacy doctor is a lightweight PII-ish heuristic scan — not legal/compliance advice (not GDPR/HIPAA/PCI attestation).",
33
+ "Matches are INFERRED; many are fixtures, tests, or placeholders.",
34
+ "Matched values are redacted in excerpts; never export raw PII from this report.",
35
+ ];
36
+ const findings = [];
37
+ const { files } = await loadProjectSourceFiles(root, () => true);
38
+ for (const file of files) {
39
+ for (const pattern of PII_PATTERNS) {
40
+ pattern.re.lastIndex = 0;
41
+ let match;
42
+ while ((match = pattern.re.exec(file.content)) !== null) {
43
+ const raw = match[0] ?? "";
44
+ if (pattern.id === "credit-card-like" && raw.replace(/\D/g, "").length < 13)
45
+ continue;
46
+ const line = lineNumberAt(file.content, match.index);
47
+ const lineText = file.content.split(/\r?\n/)[line - 1] ?? "";
48
+ findings.push({
49
+ id: pattern.id,
50
+ label: pattern.label,
51
+ truth: "INFERRED",
52
+ evidence: [
53
+ {
54
+ path: file.relativePath,
55
+ line,
56
+ excerpt: redactPiiExcerpt(lineText, raw),
57
+ },
58
+ ],
59
+ });
60
+ if (findings.length >= 100)
61
+ break;
62
+ }
63
+ if (findings.length >= 100)
64
+ break;
65
+ }
66
+ if (findings.length >= 100) {
67
+ limitations.push("PII finding cap reached (100)");
68
+ break;
69
+ }
70
+ }
71
+ const brain = await loadLatestBrain(root);
72
+ if (brain) {
73
+ const brainHits = searchBrain(brain, "email");
74
+ if (brainHits.length > 0) {
75
+ findings.push({
76
+ id: "brain-claim-hint",
77
+ label: "Project brain mentions email-related claims (review manually)",
78
+ truth: "INFERRED",
79
+ evidence: brainHits.slice(0, 3).map((h) => ({
80
+ path: ".agentdoctor/project-brain",
81
+ excerpt: h.text.slice(0, 100),
82
+ })),
83
+ });
84
+ }
85
+ }
86
+ else {
87
+ limitations.push("Project brain not available for supplemental search.");
88
+ }
89
+ return { root, findings, limitations };
90
+ }
@@ -0,0 +1,21 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface RequirementTrace {
3
+ id: string;
4
+ title: string;
5
+ sourcePath: string;
6
+ sourceLine: number;
7
+ acceptanceCriteria: string[];
8
+ truth: TruthLabel;
9
+ evidence: ProductEvidence[];
10
+ missingTestHeuristic: boolean;
11
+ missingTestNote?: string;
12
+ }
13
+ export interface RequirementsTraceReport {
14
+ root: string;
15
+ requirements: RequirementTrace[];
16
+ limitations: string[];
17
+ }
18
+ /**
19
+ * Scan repository docs for requirements / user stories with file evidence only.
20
+ */
21
+ export declare function traceRequirements(rootInput: string, maxFileSizeBytes?: number): Promise<RequirementsTraceReport>;