@agentlighthouse/core 0.1.0-alpha.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/dist/analyzers/mcp.d.ts +8 -0
  4. package/dist/analyzers/mcp.d.ts.map +1 -0
  5. package/dist/analyzers/mcp.js +214 -0
  6. package/dist/analyzers/openapi.d.ts +7 -0
  7. package/dist/analyzers/openapi.d.ts.map +1 -0
  8. package/dist/analyzers/openapi.js +344 -0
  9. package/dist/analyzers/readiness.d.ts +8 -0
  10. package/dist/analyzers/readiness.d.ts.map +1 -0
  11. package/dist/analyzers/readiness.js +766 -0
  12. package/dist/analyzers/tasks.d.ts +3 -0
  13. package/dist/analyzers/tasks.d.ts.map +1 -0
  14. package/dist/analyzers/tasks.js +140 -0
  15. package/dist/changes/files.d.ts +5 -0
  16. package/dist/changes/files.d.ts.map +1 -0
  17. package/dist/changes/files.js +71 -0
  18. package/dist/comparison/compare.d.ts +14 -0
  19. package/dist/comparison/compare.d.ts.map +1 -0
  20. package/dist/comparison/compare.js +323 -0
  21. package/dist/config/profile.d.ts +16 -0
  22. package/dist/config/profile.d.ts.map +1 -0
  23. package/dist/config/profile.js +47 -0
  24. package/dist/detection/project.d.ts +4 -0
  25. package/dist/detection/project.d.ts.map +1 -0
  26. package/dist/detection/project.js +225 -0
  27. package/dist/findings/helpers.d.ts +36 -0
  28. package/dist/findings/helpers.d.ts.map +1 -0
  29. package/dist/findings/helpers.js +115 -0
  30. package/dist/findings/locations.d.ts +4 -0
  31. package/dist/findings/locations.d.ts.map +1 -0
  32. package/dist/findings/locations.js +117 -0
  33. package/dist/generators/artifacts.d.ts +6 -0
  34. package/dist/generators/artifacts.d.ts.map +1 -0
  35. package/dist/generators/artifacts.js +255 -0
  36. package/dist/index.d.ts +486 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +451 -0
  39. package/dist/probes/commands.d.ts +7 -0
  40. package/dist/probes/commands.d.ts.map +1 -0
  41. package/dist/probes/commands.js +198 -0
  42. package/dist/reporters/cli.d.ts +4 -0
  43. package/dist/reporters/cli.d.ts.map +1 -0
  44. package/dist/reporters/cli.js +42 -0
  45. package/dist/reporters/comparison.d.ts +13 -0
  46. package/dist/reporters/comparison.d.ts.map +1 -0
  47. package/dist/reporters/comparison.js +227 -0
  48. package/dist/reporters/github-summary.d.ts +4 -0
  49. package/dist/reporters/github-summary.d.ts.map +1 -0
  50. package/dist/reporters/github-summary.js +4 -0
  51. package/dist/reporters/json.d.ts +3 -0
  52. package/dist/reporters/json.d.ts.map +1 -0
  53. package/dist/reporters/json.js +3 -0
  54. package/dist/reporters/markdown.d.ts +3 -0
  55. package/dist/reporters/markdown.d.ts.map +1 -0
  56. package/dist/reporters/markdown.js +146 -0
  57. package/dist/reporters/pr-summary.d.ts +8 -0
  58. package/dist/reporters/pr-summary.d.ts.map +1 -0
  59. package/dist/reporters/pr-summary.js +38 -0
  60. package/dist/reporters/sarif.d.ts +3 -0
  61. package/dist/reporters/sarif.d.ts.map +1 -0
  62. package/dist/reporters/sarif.js +119 -0
  63. package/dist/reporters/shared.d.ts +8 -0
  64. package/dist/reporters/shared.d.ts.map +1 -0
  65. package/dist/reporters/shared.js +26 -0
  66. package/dist/scanners/filesystem.d.ts +6 -0
  67. package/dist/scanners/filesystem.d.ts.map +1 -0
  68. package/dist/scanners/filesystem.js +231 -0
  69. package/dist/schemas/types.d.ts +6652 -0
  70. package/dist/schemas/types.d.ts.map +1 -0
  71. package/dist/schemas/types.js +383 -0
  72. package/dist/scoring/calibration.d.ts +18 -0
  73. package/dist/scoring/calibration.d.ts.map +1 -0
  74. package/dist/scoring/calibration.js +231 -0
  75. package/dist/scoring/model.d.ts +21 -0
  76. package/dist/scoring/model.d.ts.map +1 -0
  77. package/dist/scoring/model.js +109 -0
  78. package/package.json +58 -0
@@ -0,0 +1,3 @@
1
+ import type { Finding, ProjectSignals } from "../schemas/types.js";
2
+ export declare function analyzeTaskBenchmarks(signals: ProjectSignals): Finding[];
3
+ //# sourceMappingURL=tasks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/analyzers/tasks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAkBnE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,EAAE,CA2FxE"}
@@ -0,0 +1,140 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ import { finding } from "../findings/helpers.js";
3
+ export function analyzeTaskBenchmarks(signals) {
4
+ if (signals.benchmarkFiles.length === 0) {
5
+ return [
6
+ finding({
7
+ id: "TASK_BENCHMARK_MISSING",
8
+ title: "Missing agent task benchmark file",
9
+ severity: "medium",
10
+ category: "task_benchmarks",
11
+ description: "The project has no agentlighthouse.tasks.yaml or benchmark task file.",
12
+ evidence: ["No agent task benchmark file was found."],
13
+ recommendation: "Add agentlighthouse.tasks.yaml with realistic, verifiable agent workflows.",
14
+ agentFailureMode: "A team cannot tell whether agents can complete common workflows because no deterministic task set exists.",
15
+ fixExample: "Add tasks for installing the project, running tests, adding a small feature, and using the public API.",
16
+ affectedFile: "agentlighthouse.tasks.yaml",
17
+ suggestedFixType: "create_file"
18
+ })
19
+ ];
20
+ }
21
+ const tasks = signals.benchmarkFiles.flatMap((file) => parseTasks(file, signals.textByPath[file]));
22
+ const findings = [];
23
+ pushTaskFinding(findings, {
24
+ id: "TASK_OBJECTIVE_TOO_VAGUE",
25
+ title: "Benchmark tasks have vague objectives",
26
+ severity: "medium",
27
+ tasks: tasks.filter((task) => !task.objective || task.objective.length < 30),
28
+ recommendation: "Write task objectives as concrete user goals with project-specific context.",
29
+ agentFailureMode: "A coding agent may optimize for the wrong outcome because the task goal is underspecified.",
30
+ fixExample: "Objective: Add cursor pagination to GET /customers and update the SDK example."
31
+ });
32
+ pushTaskFinding(findings, {
33
+ id: "TASK_SUCCESS_CRITERIA_MISSING",
34
+ title: "Benchmark tasks lack success criteria",
35
+ severity: "medium",
36
+ tasks: tasks.filter((task) => task.successCriteria.length === 0),
37
+ recommendation: "Add successCriteria items that can be checked by humans or deterministic commands.",
38
+ agentFailureMode: "A task may look complete even when tests, docs, or expected files are missing.",
39
+ fixExample: "Success criteria: unit tests pass, docs mention the new flag, and output includes next_cursor."
40
+ });
41
+ pushTaskFinding(findings, {
42
+ id: "TASK_VERIFICATION_MISSING",
43
+ title: "Benchmark tasks lack verification commands",
44
+ severity: "medium",
45
+ tasks: tasks.filter((task) => task.verificationCommands.length === 0),
46
+ recommendation: "Add verificationCommands for tests, typecheck, lint, build, or smoke checks.",
47
+ agentFailureMode: "Agent success cannot be validated without a repeatable command or inspection step.",
48
+ fixExample: "verificationCommands: ['pnpm test', 'pnpm typecheck']"
49
+ });
50
+ pushTaskFinding(findings, {
51
+ id: "TASK_RISK_UNMARKED",
52
+ title: "Risky benchmark tasks do not mark risk level",
53
+ severity: "low",
54
+ tasks: tasks.filter((task) => taskLooksRisky(task) && !task.riskLevel),
55
+ recommendation: "Set riskLevel for tasks that touch auth, secrets, writes, deletes, billing, or external APIs.",
56
+ agentFailureMode: "A coding agent may perform risky operations without extra review expectations.",
57
+ fixExample: "riskLevel: high for tasks that mutate production-like data or handle credentials."
58
+ });
59
+ pushTaskFinding(findings, {
60
+ id: "TASK_REQUIRED_DOCS_MISSING",
61
+ title: "Benchmark tasks do not name required docs",
62
+ severity: "low",
63
+ tasks: tasks.filter((task) => task.requiredDocs.length === 0),
64
+ recommendation: "List requiredDocs so agents know which instructions and docs should be sufficient.",
65
+ agentFailureMode: "A task may pass only because an agent guessed from source code instead of using intended docs.",
66
+ fixExample: "requiredDocs: ['README.md', 'docs/API.md', 'AGENTS.md']"
67
+ });
68
+ pushTaskFinding(findings, {
69
+ id: "TASK_FAILURE_MODES_MISSING",
70
+ title: "Benchmark tasks lack common failure modes",
71
+ severity: "low",
72
+ tasks: tasks.filter((task) => task.commonFailureModes.length === 0),
73
+ recommendation: "Document common mistakes agents make on each task.",
74
+ agentFailureMode: "Reviewers cannot distinguish robust task completion from lucky partial completion.",
75
+ fixExample: "commonFailureModes: ['forgets auth header', 'updates generated files directly']"
76
+ });
77
+ return findings;
78
+ }
79
+ function parseTasks(file, content) {
80
+ if (!content)
81
+ return [];
82
+ try {
83
+ const parsed = parseYaml(content);
84
+ const rawTasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
85
+ return rawTasks
86
+ .filter((task) => Boolean(task) && typeof task === "object")
87
+ .map((task, index) => ({
88
+ file,
89
+ id: stringValue(task.id) ?? `task-${index + 1}`,
90
+ title: stringValue(task.title),
91
+ persona: stringValue(task.persona),
92
+ objective: stringValue(task.objective) ?? stringValue(task.prompt),
93
+ requiredDocs: stringArray(task.requiredDocs),
94
+ expectedOutputs: stringArray(task.expectedOutputs),
95
+ successCriteria: stringArray(task.successCriteria).concat(stringArray(task.success_criteria)),
96
+ verificationCommands: stringArray(task.verificationCommands),
97
+ riskLevel: stringValue(task.riskLevel),
98
+ commonFailureModes: stringArray(task.commonFailureModes)
99
+ }));
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ function pushTaskFinding(findings, input) {
106
+ if (input.tasks.length === 0)
107
+ return;
108
+ findings.push(finding({
109
+ id: input.id,
110
+ title: input.title,
111
+ severity: input.severity,
112
+ category: "task_benchmarks",
113
+ description: `${input.tasks.length} benchmark task(s) need stronger verification metadata.`,
114
+ evidence: input.tasks.slice(0, 8).map((task) => `${task.file}: ${task.id}`),
115
+ recommendation: input.recommendation,
116
+ agentFailureMode: input.agentFailureMode,
117
+ fixExample: input.fixExample,
118
+ affectedFile: input.tasks[0]?.file,
119
+ suggestedFixType: "update_file"
120
+ }));
121
+ }
122
+ function taskLooksRisky(task) {
123
+ const haystack = [
124
+ task.title,
125
+ task.objective,
126
+ task.expectedOutputs.join(" "),
127
+ task.successCriteria.join(" ")
128
+ ]
129
+ .join(" ")
130
+ .toLowerCase();
131
+ return /(delete|write|secret|token|auth|billing|payment|external|production|customer)/i.test(haystack);
132
+ }
133
+ function stringArray(value) {
134
+ return Array.isArray(value)
135
+ ? value.filter((entry) => typeof entry === "string")
136
+ : [];
137
+ }
138
+ function stringValue(value) {
139
+ return typeof value === "string" ? value : undefined;
140
+ }
@@ -0,0 +1,5 @@
1
+ import type { ChangedFile, ChangedFileSource } from "../schemas/types.js";
2
+ export declare function normalizeChangedPath(filePath: string): string;
3
+ export declare function parseChangedFilesText(content: string, source?: ChangedFileSource): ChangedFile[];
4
+ export declare function parseGitNameStatus(output: string, source?: ChangedFileSource): ChangedFile[];
5
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/changes/files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAqB,MAAM,qBAAqB,CAAC;AAU7F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,iBAA8B,GACrC,WAAW,EAAE,CAOf;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,iBAAyB,GAChC,WAAW,EAAE,CAKf"}
@@ -0,0 +1,71 @@
1
+ const gitStatusMap = {
2
+ A: "added",
3
+ M: "modified",
4
+ D: "deleted",
5
+ R: "renamed",
6
+ C: "copied"
7
+ };
8
+ export function normalizeChangedPath(filePath) {
9
+ return filePath
10
+ .trim()
11
+ .replaceAll("\\", "/")
12
+ .replace(/^\.\/+/, "");
13
+ }
14
+ export function parseChangedFilesText(content, source = "explicit") {
15
+ return content
16
+ .split(/\r?\n/)
17
+ .map((line) => line.trim())
18
+ .filter(Boolean)
19
+ .map((line) => parseChangedFileLine(line, source))
20
+ .filter((file) => file.path);
21
+ }
22
+ export function parseGitNameStatus(output, source = "git") {
23
+ if (output.includes("\0")) {
24
+ return parseNullDelimitedNameStatus(output, source);
25
+ }
26
+ return parseChangedFilesText(output, source);
27
+ }
28
+ function parseNullDelimitedNameStatus(output, source) {
29
+ const tokens = output.split("\0").filter(Boolean);
30
+ const files = [];
31
+ for (let index = 0; index < tokens.length; index += 1) {
32
+ const statusToken = tokens[index] ?? "";
33
+ const status = mapStatus(statusToken);
34
+ if (status === "renamed" || status === "copied") {
35
+ files.push({
36
+ path: normalizeChangedPath(tokens[index + 2] ?? ""),
37
+ oldPath: normalizeChangedPath(tokens[index + 1] ?? ""),
38
+ status,
39
+ source
40
+ });
41
+ index += 2;
42
+ continue;
43
+ }
44
+ files.push({
45
+ path: normalizeChangedPath(tokens[index + 1] ?? statusToken),
46
+ status,
47
+ source
48
+ });
49
+ index += 1;
50
+ }
51
+ return files.filter((file) => file.path);
52
+ }
53
+ function parseChangedFileLine(line, source) {
54
+ const parts = line.split("\t");
55
+ if (parts.length >= 2) {
56
+ const status = mapStatus(parts[0] ?? "");
57
+ if ((status === "renamed" || status === "copied") && parts.length >= 3) {
58
+ return {
59
+ path: normalizeChangedPath(parts[2] ?? ""),
60
+ oldPath: normalizeChangedPath(parts[1] ?? ""),
61
+ status,
62
+ source
63
+ };
64
+ }
65
+ return { path: normalizeChangedPath(parts[1] ?? ""), status, source };
66
+ }
67
+ return { path: normalizeChangedPath(line), status: "modified", source };
68
+ }
69
+ function mapStatus(statusToken) {
70
+ return gitStatusMap[statusToken.charAt(0).toUpperCase()] ?? "unknown";
71
+ }
@@ -0,0 +1,14 @@
1
+ import type { ChangedFile, ComparisonFinding, ComparisonResult, Finding, PrImpact, ScanResult } from "../schemas/types.js";
2
+ export declare const comparisonModelVersion = "0.1.0";
3
+ export declare function compareScanResults(baseline: ScanResult, current: ScanResult, options?: {
4
+ changedFiles?: ChangedFile[];
5
+ }): ComparisonResult;
6
+ export declare function classifyPrImpact(changedFiles: ChangedFile[], findings: {
7
+ newFindings: ComparisonFinding[];
8
+ resolvedFindings: ComparisonFinding[];
9
+ unchangedFindings: ComparisonFinding[];
10
+ worsenedFindings: ComparisonFinding[];
11
+ improvedFindings: ComparisonFinding[];
12
+ }): PrImpact;
13
+ export declare function ensureFindingIdentity(finding: Finding): ComparisonFinding;
14
+ //# sourceMappingURL=compare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../../src/comparison/compare.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAEhB,OAAO,EACP,QAAQ,EACR,UAAU,EAEX,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,sBAAsB,UAAU,CAAC;AAI9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,UAAU,EACnB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,WAAW,EAAE,CAAA;CAAO,GAC7C,gBAAgB,CAgHlB;AAED,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE;IACR,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,iBAAiB,EAAE,iBAAiB,EAAE,CAAC;IACvC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;CACvC,GACA,QAAQ,CAkDV;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,iBAAiB,CAmBzE"}
@@ -0,0 +1,323 @@
1
+ import { deriveFindingIdentity, stableFingerprint } from "../findings/helpers.js";
2
+ import { normalizeChangedPath } from "../changes/files.js";
3
+ import { confidenceRank, severityRank } from "../reporters/shared.js";
4
+ export const comparisonModelVersion = "0.1.0";
5
+ const severities = ["critical", "high", "medium", "low", "info"];
6
+ export function compareScanResults(baseline, current, options = {}) {
7
+ const baselineFindings = baseline.findings.map(ensureFindingIdentity);
8
+ const currentFindings = current.findings.map(ensureFindingIdentity);
9
+ const baselineByFingerprint = mapByFingerprint(baselineFindings);
10
+ const currentByFingerprint = mapByFingerprint(currentFindings);
11
+ const newFindings = [];
12
+ const resolvedFindings = [];
13
+ const unchangedFindings = [];
14
+ const worsenedFindings = [];
15
+ const improvedFindings = [];
16
+ for (const finding of currentFindings) {
17
+ const previous = baselineByFingerprint.get(finding.fingerprint ?? "");
18
+ if (!previous) {
19
+ newFindings.push(finding);
20
+ continue;
21
+ }
22
+ if (severityRank(finding.severity) > severityRank(previous.severity)) {
23
+ worsenedFindings.push({
24
+ ...finding,
25
+ previousSeverity: previous.severity,
26
+ currentSeverity: finding.severity
27
+ });
28
+ }
29
+ else if (severityRank(finding.severity) < severityRank(previous.severity)) {
30
+ improvedFindings.push({
31
+ ...finding,
32
+ previousSeverity: previous.severity,
33
+ currentSeverity: finding.severity
34
+ });
35
+ }
36
+ else {
37
+ unchangedFindings.push(finding);
38
+ }
39
+ }
40
+ for (const finding of baselineFindings) {
41
+ if (!currentByFingerprint.has(finding.fingerprint ?? "")) {
42
+ resolvedFindings.push(finding);
43
+ }
44
+ }
45
+ const scoreDelta = current.score - baseline.score;
46
+ const confidenceDelta = current.scoreConfidenceScore - baseline.scoreConfidenceScore;
47
+ const coverageDelta = current.coverage.coveragePercent - baseline.coverage.coveragePercent;
48
+ const severityCountDeltas = severityDeltas(baselineFindings, currentFindings);
49
+ const caveats = comparisonCaveats(baseline, current, baselineFindings, currentFindings);
50
+ const regressionDetected = scoreDelta < 0 ||
51
+ coverageDelta < 0 ||
52
+ confidenceDelta < 0 ||
53
+ newFindings.some((finding) => severityRank(finding.severity) >= severityRank("high")) ||
54
+ worsenedFindings.length > 0;
55
+ const improvementDetected = scoreDelta > 0 || resolvedFindings.length > 0 || improvedFindings.length > 0;
56
+ const prImpact = options.changedFiles && options.changedFiles.length > 0
57
+ ? classifyPrImpact(options.changedFiles, {
58
+ newFindings,
59
+ resolvedFindings,
60
+ unchangedFindings,
61
+ worsenedFindings,
62
+ improvedFindings
63
+ })
64
+ : undefined;
65
+ const verdict = caveats.some((caveat) => caveat.includes("incompatible")) ||
66
+ (baselineFindings.length + currentFindings.length > 0 &&
67
+ [...baselineFindings, ...currentFindings].some((finding) => !finding.fingerprint))
68
+ ? "inconclusive"
69
+ : regressionDetected && improvementDetected
70
+ ? scoreDelta >= 0
71
+ ? "mixed"
72
+ : "regressed"
73
+ : regressionDetected
74
+ ? "regressed"
75
+ : improvementDetected
76
+ ? "improved"
77
+ : "unchanged";
78
+ return {
79
+ comparisonId: createComparisonId(baseline, current),
80
+ baseline: scanSnapshot(baseline),
81
+ current: scanSnapshot(current),
82
+ deltas: {
83
+ scoreDelta,
84
+ confidenceDelta,
85
+ coverageDelta,
86
+ findingCountDelta: currentFindings.length - baselineFindings.length,
87
+ severityCountDeltas
88
+ },
89
+ findings: {
90
+ new: sortFindings(newFindings),
91
+ resolved: sortFindings(resolvedFindings),
92
+ unchanged: sortFindings(unchangedFindings),
93
+ worsened: sortFindings(worsenedFindings),
94
+ improved: sortFindings(improvedFindings)
95
+ },
96
+ ...(prImpact ? { prImpact } : {}),
97
+ summary: {
98
+ verdict,
99
+ regressionDetected,
100
+ improvementDetected,
101
+ topRegressions: topRegressions(newFindings, worsenedFindings, scoreDelta, coverageDelta),
102
+ topImprovements: topImprovements(resolvedFindings, improvedFindings, scoreDelta),
103
+ recommendedActions: recommendedActions(newFindings, worsenedFindings, scoreDelta),
104
+ caveats
105
+ },
106
+ metadata: {
107
+ agentLighthouseVersion: current.agentLighthouseVersion,
108
+ comparisonModelVersion
109
+ }
110
+ };
111
+ }
112
+ export function classifyPrImpact(changedFiles, findings) {
113
+ const normalizedChangedFiles = changedFiles.map((file) => ({
114
+ ...file,
115
+ path: normalizeChangedPath(file.path),
116
+ ...(file.oldPath ? { oldPath: normalizeChangedPath(file.oldPath) } : {})
117
+ }));
118
+ const changedPathSet = new Set(normalizedChangedFiles.flatMap((file) => [file.path, file.oldPath].filter(Boolean)));
119
+ const classify = (finding) => ({
120
+ ...finding,
121
+ prImpactClassification: classifyFinding(finding, changedPathSet)
122
+ });
123
+ const newClassified = findings.newFindings.map(classify);
124
+ const resolvedClassified = findings.resolvedFindings.map(classify);
125
+ const unchangedClassified = findings.unchangedFindings.map(classify);
126
+ const changedNew = newClassified.filter(isChangedImpact);
127
+ const changedResolved = resolvedClassified.filter(isChangedImpact);
128
+ const changedUnchanged = unchangedClassified.filter(isChangedImpact);
129
+ const globalNew = newClassified.filter((finding) => finding.prImpactClassification === "global");
130
+ const globalResolved = resolvedClassified.filter((finding) => finding.prImpactClassification === "global");
131
+ const unknown = [...newClassified, ...resolvedClassified, ...unchangedClassified].filter((finding) => finding.prImpactClassification === "unknown");
132
+ const unrelatedExisting = unchangedClassified.filter((finding) => finding.prImpactClassification === "unrelated");
133
+ const impactedFiles = [...changedNew, ...changedResolved, ...changedUnchanged]
134
+ .map((finding) => normalizeFindingFile(finding))
135
+ .filter((file) => Boolean(file));
136
+ const highestChangedFileSeverity = [...changedNew, ...changedResolved]
137
+ .map((finding) => finding.severity)
138
+ .sort((a, b) => severityRank(b) - severityRank(a))[0];
139
+ return {
140
+ changedFiles: normalizedChangedFiles,
141
+ changedFileCount: normalizedChangedFiles.length,
142
+ newFindingsOnChangedFiles: sortFindings(changedNew),
143
+ resolvedFindingsOnChangedFiles: sortFindings(changedResolved),
144
+ unchangedFindingsOnChangedFiles: sortFindings(changedUnchanged),
145
+ globalNewFindings: sortFindings(globalNew),
146
+ globalResolvedFindings: sortFindings(globalResolved),
147
+ unknownLocationFindings: sortFindings(unknown),
148
+ unrelatedExistingFindings: sortFindings(unrelatedExisting),
149
+ filesWithAgentReadinessImpact: [...new Set(impactedFiles)].sort(),
150
+ ...(highestChangedFileSeverity ? { highestChangedFileSeverity } : {}),
151
+ summary: summarizePrImpact(changedNew, globalNew, unknown, normalizedChangedFiles.length)
152
+ };
153
+ }
154
+ export function ensureFindingIdentity(finding) {
155
+ if (finding.fingerprint && finding.identityParts && finding.locationKey && finding.subject) {
156
+ return finding;
157
+ }
158
+ const identity = deriveFindingIdentity({
159
+ ruleId: finding.ruleId,
160
+ affectedFile: finding.affectedFile,
161
+ evidence: finding.evidence,
162
+ subject: finding.subject,
163
+ locationKey: finding.locationKey,
164
+ identityParts: finding.identityParts
165
+ });
166
+ return {
167
+ ...finding,
168
+ identityParts: finding.identityParts ?? identity.identityParts,
169
+ locationKey: finding.locationKey ?? identity.locationKey,
170
+ subject: finding.subject ?? identity.subject,
171
+ fingerprint: finding.fingerprint ?? stableFingerprint(identity.identityParts)
172
+ };
173
+ }
174
+ function scanSnapshot(result) {
175
+ return {
176
+ scanId: result.scanId,
177
+ scannedPath: result.scannedPath,
178
+ score: result.score,
179
+ confidence: result.scoreConfidence,
180
+ confidenceScore: result.scoreConfidenceScore,
181
+ coverage: result.coverage.coveragePercent,
182
+ profile: result.profile,
183
+ completedAt: result.completedAt
184
+ };
185
+ }
186
+ function mapByFingerprint(findings) {
187
+ const map = new Map();
188
+ for (const finding of findings) {
189
+ if (finding.fingerprint) {
190
+ map.set(finding.fingerprint, finding);
191
+ }
192
+ }
193
+ return map;
194
+ }
195
+ function severityDeltas(baseline, current) {
196
+ const baselineCounts = severityCounts(baseline);
197
+ const currentCounts = severityCounts(current);
198
+ return Object.fromEntries(severities.map((severity) => [severity, currentCounts[severity] - baselineCounts[severity]]));
199
+ }
200
+ function severityCounts(findings) {
201
+ return severities.reduce((counts, severity) => ({
202
+ ...counts,
203
+ [severity]: findings.filter((f) => f.severity === severity).length
204
+ }), { critical: 0, high: 0, medium: 0, low: 0, info: 0 });
205
+ }
206
+ function sortFindings(findings) {
207
+ return [...findings].sort((a, b) => {
208
+ const severityDelta = severityRank(b.severity) - severityRank(a.severity);
209
+ if (severityDelta !== 0)
210
+ return severityDelta;
211
+ return (a.ruleId + (a.locationKey ?? "")).localeCompare(b.ruleId + (b.locationKey ?? ""));
212
+ });
213
+ }
214
+ function comparisonCaveats(baseline, current, baselineFindings, currentFindings) {
215
+ const caveats = [];
216
+ if (baseline.profile !== current.profile) {
217
+ caveats.push(`Different profiles: baseline ${baseline.profile}, current ${current.profile}.`);
218
+ }
219
+ if (baseline.scoringModelVersion !== current.scoringModelVersion) {
220
+ caveats.push(`Different scoring model versions: baseline ${baseline.scoringModelVersion}, current ${current.scoringModelVersion}.`);
221
+ }
222
+ if (baseline.agentLighthouseVersion !== current.agentLighthouseVersion) {
223
+ caveats.push(`Different AgentLighthouse versions: baseline ${baseline.agentLighthouseVersion}, current ${current.agentLighthouseVersion}.`);
224
+ }
225
+ if (Math.abs(current.coverage.coveragePercent - baseline.coverage.coveragePercent) >= 25) {
226
+ caveats.push("Coverage changed substantially; score deltas may reflect changed analyzability.");
227
+ }
228
+ if (confidenceRank(baseline.scoreConfidence) === 0 ||
229
+ confidenceRank(current.scoreConfidence) === 0) {
230
+ caveats.push("One or both scans have low confidence.");
231
+ }
232
+ if ([...baselineFindings, ...currentFindings].some((finding) => !finding.fingerprint)) {
233
+ caveats.push("Some findings have missing fingerprints, making comparison incomplete.");
234
+ }
235
+ return caveats;
236
+ }
237
+ function topRegressions(newFindings, worsenedFindings, scoreDelta, coverageDelta) {
238
+ const messages = [
239
+ ...sortFindings(newFindings)
240
+ .filter((finding) => severityRank(finding.severity) >= severityRank("medium"))
241
+ .slice(0, 3)
242
+ .map((finding) => `New ${finding.severity} finding: ${finding.title}`),
243
+ ...sortFindings(worsenedFindings)
244
+ .slice(0, 3)
245
+ .map((finding) => `Worsened finding: ${finding.title} (${finding.previousSeverity} -> ${finding.currentSeverity})`)
246
+ ];
247
+ if (scoreDelta < 0)
248
+ messages.push(`Score dropped by ${Math.abs(scoreDelta)} point(s).`);
249
+ if (coverageDelta < 0)
250
+ messages.push(`Coverage dropped by ${Math.abs(coverageDelta)} point(s).`);
251
+ return messages.slice(0, 5);
252
+ }
253
+ function topImprovements(resolvedFindings, improvedFindings, scoreDelta) {
254
+ const messages = [
255
+ ...sortFindings(resolvedFindings)
256
+ .slice(0, 3)
257
+ .map((finding) => `Resolved ${finding.severity} finding: ${finding.title}`),
258
+ ...sortFindings(improvedFindings)
259
+ .slice(0, 3)
260
+ .map((finding) => `Improved finding: ${finding.title} (${finding.previousSeverity} -> ${finding.currentSeverity})`)
261
+ ];
262
+ if (scoreDelta > 0)
263
+ messages.push(`Score improved by ${scoreDelta} point(s).`);
264
+ return messages.slice(0, 5);
265
+ }
266
+ function recommendedActions(newFindings, worsenedFindings, scoreDelta) {
267
+ const actions = [];
268
+ if (newFindings.some((finding) => severityRank(finding.severity) >= severityRank("high"))) {
269
+ actions.push("Fix new high-severity agent-readiness findings before merging.");
270
+ }
271
+ if (worsenedFindings.length > 0) {
272
+ actions.push("Review worsened findings and restore the clearer baseline behavior.");
273
+ }
274
+ if (scoreDelta < 0) {
275
+ actions.push("Compare the changed files with the baseline report and recover lost context.");
276
+ }
277
+ for (const finding of sortFindings(newFindings).slice(0, 3)) {
278
+ if (!actions.includes(finding.recommendation)) {
279
+ actions.push(finding.recommendation);
280
+ }
281
+ }
282
+ return actions.slice(0, 5);
283
+ }
284
+ function createComparisonId(baseline, current) {
285
+ const input = `${baseline.scanId}:${current.scanId}:${baseline.completedAt}:${current.completedAt}`;
286
+ let hash = 2166136261;
287
+ for (const character of input) {
288
+ hash ^= character.charCodeAt(0);
289
+ hash = Math.imul(hash, 16777619);
290
+ }
291
+ return `cmp_${(hash >>> 0).toString(16).padStart(8, "0")}`;
292
+ }
293
+ function classifyFinding(finding, changedPathSet) {
294
+ const file = normalizeFindingFile(finding);
295
+ if (file && changedPathSet.has(file))
296
+ return "touched";
297
+ if (file &&
298
+ [...changedPathSet].some((changedFile) => finding.locationKey?.includes(changedFile))) {
299
+ return "related";
300
+ }
301
+ if (isGlobalFinding(finding))
302
+ return "global";
303
+ if (!file)
304
+ return "unknown";
305
+ return "unrelated";
306
+ }
307
+ function normalizeFindingFile(finding) {
308
+ const file = finding.location?.file ?? finding.affectedFile;
309
+ if (!file || file === "n/a")
310
+ return undefined;
311
+ return normalizeChangedPath(file);
312
+ }
313
+ function isChangedImpact(finding) {
314
+ return (finding.prImpactClassification === "touched" || finding.prImpactClassification === "related");
315
+ }
316
+ function isGlobalFinding(finding) {
317
+ if (!finding.affectedFile && !finding.location?.file)
318
+ return true;
319
+ return /^(COMMAND_VERIFICATION_SKIPPED|api\.openapi-not-detected|mcp\.not-evaluated)$/.test(finding.ruleId);
320
+ }
321
+ function summarizePrImpact(changedNew, globalNew, unknown, changedFileCount) {
322
+ return `${changedFileCount} changed file(s) analyzed; ${changedNew.length} new finding(s) on changed files, ${globalNew.length} new global finding(s), ${unknown.length} unknown-location finding(s).`;
323
+ }
@@ -0,0 +1,16 @@
1
+ import type { ProjectSignals, ScanOptions, ScanProfile } from "../schemas/types.js";
2
+ export interface AgentLighthouseConfig {
3
+ profile?: ScanProfile;
4
+ probes?: {
5
+ commands?: boolean;
6
+ timeoutMs?: number;
7
+ allowedScripts?: string[];
8
+ };
9
+ }
10
+ export interface ResolvedProfile {
11
+ profile: ScanProfile;
12
+ source: "cli" | "config" | "inferred";
13
+ }
14
+ export declare function resolveProfile(signals: ProjectSignals, options?: ScanOptions): ResolvedProfile;
15
+ export declare function resolveConfig(signals: ProjectSignals): AgentLighthouseConfig;
16
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/config/profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGpF,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;CACvC;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,WAAgB,GACxB,eAAe,CAgBjB;AA0BD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,qBAAqB,CAE5E"}
@@ -0,0 +1,47 @@
1
+ import { detectProject } from "../detection/project.js";
2
+ export function resolveProfile(signals, options = {}) {
3
+ if (options.profile) {
4
+ return { profile: options.profile, source: "cli" };
5
+ }
6
+ const configured = parseConfig(signals.textByPath["agentlighthouse.config.json"]);
7
+ if (configured?.profile) {
8
+ return { profile: configured.profile, source: "config" };
9
+ }
10
+ const detected = detectProject(signals);
11
+ if (detected.type === "mcp_project")
12
+ return { profile: "mcp", source: "inferred" };
13
+ if (detected.type === "openapi_project")
14
+ return { profile: "api", source: "inferred" };
15
+ if (detected.type === "docs_only")
16
+ return { profile: "docs", source: "inferred" };
17
+ if (detected.type === "node_typescript" || detected.type === "node_javascript") {
18
+ return { profile: "library", source: "inferred" };
19
+ }
20
+ return { profile: "default", source: "inferred" };
21
+ }
22
+ function parseConfig(content) {
23
+ if (!content)
24
+ return undefined;
25
+ try {
26
+ const parsed = JSON.parse(content);
27
+ if (parsed.profile === "default" ||
28
+ parsed.profile === "devtool" ||
29
+ parsed.profile === "api" ||
30
+ parsed.profile === "mcp" ||
31
+ parsed.profile === "docs" ||
32
+ parsed.profile === "library" ||
33
+ parsed.profile === "internal") {
34
+ return {
35
+ profile: parsed.profile,
36
+ probes: parsed.probes
37
+ };
38
+ }
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ return undefined;
44
+ }
45
+ export function resolveConfig(signals) {
46
+ return parseConfig(signals.textByPath["agentlighthouse.config.json"]) ?? {};
47
+ }
@@ -0,0 +1,4 @@
1
+ import type { DetectedArtifact, DetectedProject, ProjectSignals } from "../schemas/types.js";
2
+ export declare function detectProject(signals: ProjectSignals): DetectedProject;
3
+ export declare function detectedArtifacts(signals: ProjectSignals): DetectedArtifact[];
4
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/detection/project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EACf,cAAc,EAEf,MAAM,qBAAqB,CAAC;AAY7B,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,eAAe,CAsEtE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,gBAAgB,EAAE,CAY7E"}