@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,225 @@
1
+ const artifactRoles = {
2
+ "AGENTS.md": "Primary coding-agent instructions",
3
+ "CLAUDE.md": "Claude Code project memory",
4
+ "llms.txt": "LLM-readable project map",
5
+ "README.md": "Human and agent entry point",
6
+ ".cursor/rules": "Cursor rules",
7
+ ".github/copilot-instructions.md": "GitHub Copilot instructions",
8
+ ".agentlighthouseignore": "AgentLighthouse ignore rules"
9
+ };
10
+ export function detectProject(signals) {
11
+ const evidence = [];
12
+ const packageJson = signals.packageJson;
13
+ const has = (file) => signals.scannedFiles.includes(file);
14
+ const hasAny = (predicate) => signals.scannedFiles.some(predicate);
15
+ const dependencies = new Set([
16
+ ...(packageJson?.dependencies ?? []),
17
+ ...(packageJson?.devDependencies ?? [])
18
+ ]);
19
+ let type = "unknown";
20
+ let confidence = 0.3;
21
+ const rootOpenApiFiles = signals.openApiFiles.filter((file) => !file.startsWith("examples/"));
22
+ if (signals.mcpFiles.length > 0 ||
23
+ [...dependencies].some((dependency) => dependency.includes("mcp"))) {
24
+ type = "mcp_project";
25
+ confidence = 0.9;
26
+ evidence.push("MCP file or package signal detected.");
27
+ }
28
+ else if (rootOpenApiFiles.length > 0) {
29
+ type = "openapi_project";
30
+ confidence = 0.85;
31
+ evidence.push(`OpenAPI files detected: ${rootOpenApiFiles.join(", ")}.`);
32
+ }
33
+ else if (packageJson &&
34
+ (has("tsconfig.json") || has("tsconfig.base.json") || hasAny((file) => file.endsWith(".ts")))) {
35
+ type = "node_typescript";
36
+ confidence = 0.9;
37
+ evidence.push("package.json plus TypeScript config or source files detected.");
38
+ }
39
+ else if (packageJson) {
40
+ type = "node_javascript";
41
+ confidence = 0.8;
42
+ evidence.push("package.json detected.");
43
+ }
44
+ else if (has("pyproject.toml") ||
45
+ has("requirements.txt") ||
46
+ hasAny((file) => file.endsWith(".py"))) {
47
+ type = "python";
48
+ confidence = 0.85;
49
+ evidence.push("Python project files detected.");
50
+ }
51
+ else if (has("Cargo.toml")) {
52
+ type = "rust";
53
+ confidence = 0.85;
54
+ evidence.push("Cargo.toml detected.");
55
+ }
56
+ else if (has("go.mod")) {
57
+ type = "go";
58
+ confidence = 0.85;
59
+ evidence.push("go.mod detected.");
60
+ }
61
+ else if (signals.docsMarkdownFiles.length > 0 &&
62
+ signals.scannedFiles.filter((file) => /\.(ts|tsx|js|jsx|py|rs|go)$/i.test(file)).length === 0) {
63
+ type = "docs_only";
64
+ confidence = 0.75;
65
+ evidence.push("Markdown docs detected without common source-code files.");
66
+ }
67
+ return {
68
+ type,
69
+ name: packageJson?.name ?? signals.projectName,
70
+ confidence,
71
+ evidence: evidence.length > 0 ? evidence : ["No strong project-type signal detected."],
72
+ packageManager: detectPackageManager(signals),
73
+ frameworks: detectFrameworks(signals)
74
+ };
75
+ }
76
+ export function detectedArtifacts(signals) {
77
+ return Object.entries(signals.artifacts).map(([artifactPath, artifact]) => {
78
+ const content = signals.textByPath[artifactPath];
79
+ return {
80
+ path: artifactPath,
81
+ exists: artifact.exists,
82
+ kind: artifact.kind,
83
+ role: artifactRoles[artifactPath] ?? "Agent-readiness artifact",
84
+ quality: artifact.exists ? artifactQuality(artifactPath, content, signals) : "missing",
85
+ notes: artifactNotes(artifactPath, content, signals)
86
+ };
87
+ });
88
+ }
89
+ function detectPackageManager(signals) {
90
+ const declaredManager = signals.packageJson?.packageManager?.split("@")[0];
91
+ if (declaredManager === "pnpm" ||
92
+ declaredManager === "npm" ||
93
+ declaredManager === "yarn" ||
94
+ declaredManager === "bun") {
95
+ return declaredManager;
96
+ }
97
+ if (signals.scannedFiles.includes("pnpm-lock.yaml"))
98
+ return "pnpm";
99
+ if (signals.scannedFiles.includes("yarn.lock"))
100
+ return "yarn";
101
+ if (signals.scannedFiles.includes("bun.lockb") || signals.scannedFiles.includes("bun.lock"))
102
+ return "bun";
103
+ if (signals.scannedFiles.includes("package-lock.json"))
104
+ return "npm";
105
+ if (signals.scannedFiles.includes("pyproject.toml"))
106
+ return "poetry";
107
+ if (signals.scannedFiles.includes("requirements.txt"))
108
+ return "pip";
109
+ if (signals.scannedFiles.includes("Cargo.toml"))
110
+ return "cargo";
111
+ if (signals.scannedFiles.includes("go.mod"))
112
+ return "go";
113
+ if (signals.packageJson)
114
+ return "npm";
115
+ return "unknown";
116
+ }
117
+ function detectFrameworks(signals) {
118
+ const dependencies = new Set([
119
+ ...(signals.packageJson?.dependencies ?? []),
120
+ ...(signals.packageJson?.devDependencies ?? [])
121
+ ]);
122
+ const frameworks = [];
123
+ for (const [dependency, framework] of [
124
+ ["next", "Next.js"],
125
+ ["react", "React"],
126
+ ["vite", "Vite"],
127
+ ["express", "Express"],
128
+ ["fastify", "Fastify"],
129
+ ["astro", "Astro"],
130
+ ["typescript", "TypeScript"],
131
+ ["vitest", "Vitest"]
132
+ ]) {
133
+ if (dependencies.has(dependency)) {
134
+ frameworks.push(framework);
135
+ }
136
+ }
137
+ if (signals.scannedFiles.includes("pyproject.toml"))
138
+ frameworks.push("Python");
139
+ if (signals.scannedFiles.includes("Cargo.toml"))
140
+ frameworks.push("Rust");
141
+ if (signals.scannedFiles.includes("go.mod"))
142
+ frameworks.push("Go");
143
+ return [...new Set(frameworks)];
144
+ }
145
+ function artifactQuality(artifactPath, content, signals) {
146
+ if (!content && artifactPath === ".cursor/rules") {
147
+ return signals.scannedFiles.some((file) => file.startsWith(".cursor/rules/"))
148
+ ? "partial"
149
+ : "unknown";
150
+ }
151
+ if (!content)
152
+ return "unknown";
153
+ const score = qualitySignals(content, artifactPath).filter(Boolean).length;
154
+ if (score >= 8)
155
+ return "strong";
156
+ if (score >= 4)
157
+ return "partial";
158
+ return "thin";
159
+ }
160
+ function artifactNotes(artifactPath, content, signals) {
161
+ if (!content &&
162
+ artifactPath === ".cursor/rules" &&
163
+ signals.scannedFiles.some((file) => file.startsWith(".cursor/rules/"))) {
164
+ return ["Cursor rules directory detected."];
165
+ }
166
+ if (!content)
167
+ return ["Artifact exists but no text content was available for quality checks."];
168
+ const checks = qualityCheckLabels(content, artifactPath);
169
+ const present = checks
170
+ .filter((check) => check.present)
171
+ .map((check) => `Includes ${check.label}.`);
172
+ const missing = checks
173
+ .filter((check) => !check.present)
174
+ .map((check) => `Missing ${check.label}.`);
175
+ return [...present.slice(0, 4), ...missing.slice(0, 4)];
176
+ }
177
+ function qualitySignals(content, artifactPath) {
178
+ return qualityCheckLabels(content, artifactPath).map((check) => check.present);
179
+ }
180
+ function qualityCheckLabels(content, artifactPath) {
181
+ const lower = content.toLowerCase();
182
+ const hasAny = (terms) => terms.some((term) => lower.includes(term));
183
+ const hasCommand = /\b(pnpm|npm|yarn|bun|pip|poetry|cargo|go)\s+[a-z0-9:_-]+/i.test(content);
184
+ const hasLink = /\[[^\]]+\]\([^)]+\)/.test(content) || /^https?:\/\//m.test(content);
185
+ const checks = [
186
+ {
187
+ label: "project overview",
188
+ present: hasAny(["overview", "purpose", "what it does", "project"])
189
+ },
190
+ { label: "setup commands", present: hasAny(["install", "setup"]) && hasCommand },
191
+ { label: "test commands", present: hasAny(["test", "pytest", "vitest", "jest"]) && hasCommand },
192
+ {
193
+ label: "lint/typecheck commands",
194
+ present: hasAny(["lint", "typecheck", "type check", "mypy"])
195
+ },
196
+ { label: "architecture map", present: hasAny(["architecture", "packages/", "apps/", "src/"]) },
197
+ { label: "coding conventions", present: hasAny(["convention", "style", "format", "naming"]) },
198
+ {
199
+ label: "security/privacy guidance",
200
+ present: hasAny(["secret", "privacy", "credential", "sensitive", "security"])
201
+ },
202
+ { label: "common mistakes", present: hasAny(["avoid", "do not", "don't", "common mistake"]) },
203
+ {
204
+ label: "generated-file warnings",
205
+ present: hasAny(["generated", "dist", "build output", "do not edit"])
206
+ },
207
+ { label: "docs links", present: hasLink || hasAny(["docs/", "readme"]) },
208
+ { label: "examples", present: hasAny(["example", "usage", "sample"]) },
209
+ { label: "task workflows", present: hasAny(["workflow", "task", "steps", "success criteria"]) }
210
+ ];
211
+ if (artifactPath === "llms.txt") {
212
+ return checks.filter((check) => ["project overview", "docs links", "examples", "architecture map"].includes(check.label));
213
+ }
214
+ if (artifactPath === "README.md") {
215
+ return checks.filter((check) => [
216
+ "project overview",
217
+ "setup commands",
218
+ "test commands",
219
+ "docs links",
220
+ "examples",
221
+ "architecture map"
222
+ ].includes(check.label));
223
+ }
224
+ return checks;
225
+ }
@@ -0,0 +1,36 @@
1
+ import type { Finding, FindingCategory, Severity, SuggestedFixType } from "../schemas/types.js";
2
+ export declare function finding(input: {
3
+ id: string;
4
+ ruleId?: string;
5
+ fingerprint?: string;
6
+ identityParts?: string[];
7
+ locationKey?: string;
8
+ subject?: string;
9
+ title: string;
10
+ severity: Severity;
11
+ category: FindingCategory;
12
+ description: string;
13
+ evidence: string[];
14
+ recommendation: string;
15
+ affectedFile?: string;
16
+ agentFailureMode?: string;
17
+ fixExample?: string;
18
+ docsLinks?: string[];
19
+ suggestedFixType: SuggestedFixType;
20
+ }): Finding;
21
+ export declare function textIncludesAny(text: string | undefined, needles: readonly string[]): boolean;
22
+ export declare function hasUsefulMarkdownLinks(text: string | undefined): boolean;
23
+ export declare function deriveFindingIdentity(input: {
24
+ ruleId: string;
25
+ affectedFile?: string;
26
+ evidence: string[];
27
+ subject?: string;
28
+ locationKey?: string;
29
+ identityParts?: string[];
30
+ }): {
31
+ identityParts: string[];
32
+ locationKey: string;
33
+ subject: string;
34
+ };
35
+ export declare function stableFingerprint(identityParts: string[]): string;
36
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/findings/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAMhG,wBAAgB,OAAO,CAAC,KAAK,EAAE;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,gBAAgB,EAAE,gBAAgB,CAAC;CACpC,GAAG,OAAO,CAkBV;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM7F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKxE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B,GAAG;IAAE,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAqDpE;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAQjE"}
@@ -0,0 +1,115 @@
1
+ const openApiEvidencePattern = /(?<file>[^:;\n]+):\s*(?<method>GET|POST|PUT|PATCH|DELETE)\s+(?<path>\/[^\s;(]+)/i;
2
+ const mcpOrTaskEvidencePattern = /(?<file>[^:;\n]+):\s*(?<subject>[A-Za-z0-9_.:-]+)/;
3
+ export function finding(input) {
4
+ const ruleId = input.ruleId ?? input.id;
5
+ const identity = deriveFindingIdentity({
6
+ ruleId,
7
+ affectedFile: input.affectedFile,
8
+ evidence: input.evidence,
9
+ subject: input.subject,
10
+ locationKey: input.locationKey,
11
+ identityParts: input.identityParts
12
+ });
13
+ return {
14
+ ...input,
15
+ ruleId,
16
+ fingerprint: input.fingerprint ?? stableFingerprint(identity.identityParts),
17
+ identityParts: input.identityParts ?? identity.identityParts,
18
+ locationKey: input.locationKey ?? identity.locationKey,
19
+ subject: input.subject ?? identity.subject
20
+ };
21
+ }
22
+ export function textIncludesAny(text, needles) {
23
+ if (!text) {
24
+ return false;
25
+ }
26
+ const lower = text.toLowerCase();
27
+ return needles.some((needle) => lower.includes(needle.toLowerCase()));
28
+ }
29
+ export function hasUsefulMarkdownLinks(text) {
30
+ if (!text) {
31
+ return false;
32
+ }
33
+ return /\[[^\]]+\]\([^)]+\)/.test(text) || /^https?:\/\//m.test(text) || /^\//m.test(text);
34
+ }
35
+ export function deriveFindingIdentity(input) {
36
+ if (input.identityParts && input.identityParts.length > 0) {
37
+ const normalizedParts = input.identityParts.map(normalizeIdentityPart);
38
+ return {
39
+ identityParts: normalizedParts,
40
+ locationKey: input.locationKey ?? normalizedParts[1] ?? input.ruleId,
41
+ subject: input.subject ?? normalizedParts.at(-1) ?? input.ruleId
42
+ };
43
+ }
44
+ const evidenceKey = firstEvidenceKey(input.evidence);
45
+ const openApiMatch = input.evidence.join("; ").match(openApiEvidencePattern);
46
+ if (openApiMatch?.groups) {
47
+ const file = normalizePath(openApiMatch.groups.file ?? input.affectedFile ?? "");
48
+ const method = (openApiMatch.groups.method ?? "").toUpperCase();
49
+ const apiPath = openApiMatch.groups.path ?? "";
50
+ const subject = input.subject ?? `${method} ${apiPath}`;
51
+ const locationKey = input.locationKey ?? `${file}#/paths/${jsonPointerPath(apiPath)}/${method.toLowerCase()}`;
52
+ return {
53
+ identityParts: [input.ruleId, locationKey, normalizeIdentityPart(subject)],
54
+ locationKey,
55
+ subject
56
+ };
57
+ }
58
+ if (/^MCP_/.test(input.ruleId) || /^TASK_/.test(input.ruleId)) {
59
+ const parsed = input.evidence.join("; ").match(mcpOrTaskEvidencePattern);
60
+ if (parsed?.groups) {
61
+ const file = normalizePath(parsed.groups.file ?? input.affectedFile ?? "");
62
+ const subject = input.subject ?? parsed.groups.subject ?? input.ruleId;
63
+ const prefix = input.ruleId.startsWith("TASK_") ? "task" : "tool";
64
+ const locationKey = input.locationKey ?? `${file}#${prefix}:${normalizeIdentityPart(subject)}`;
65
+ return {
66
+ identityParts: [input.ruleId, locationKey, normalizeIdentityPart(subject)],
67
+ locationKey,
68
+ subject
69
+ };
70
+ }
71
+ }
72
+ const locationKey = input.locationKey ?? normalizePath(input.affectedFile ?? evidenceKey);
73
+ const subject = input.subject ?? normalizeSubject(input.affectedFile ?? evidenceKey);
74
+ return {
75
+ identityParts: [
76
+ input.ruleId,
77
+ normalizeIdentityPart(locationKey || "project"),
78
+ normalizeIdentityPart(subject || input.ruleId)
79
+ ],
80
+ locationKey: locationKey || input.ruleId,
81
+ subject: subject || input.ruleId
82
+ };
83
+ }
84
+ export function stableFingerprint(identityParts) {
85
+ const input = identityParts.map(normalizeIdentityPart).join("|");
86
+ let hash = 2166136261;
87
+ for (const character of input) {
88
+ hash ^= character.charCodeAt(0);
89
+ hash = Math.imul(hash, 16777619);
90
+ }
91
+ return `alh_${(hash >>> 0).toString(16).padStart(8, "0")}`;
92
+ }
93
+ function firstEvidenceKey(evidence) {
94
+ return evidence[0] ?? "project";
95
+ }
96
+ function normalizePath(value) {
97
+ return value
98
+ .trim()
99
+ .replaceAll("\\", "/")
100
+ .replace(/^\.\/+/, "")
101
+ .replace(/^<repo>\//, "")
102
+ .toLowerCase();
103
+ }
104
+ function normalizeSubject(value) {
105
+ return value.trim().replace(/\s+/g, " ");
106
+ }
107
+ function normalizeIdentityPart(value) {
108
+ return normalizeSubject(value).replaceAll("\\", "/").toLowerCase();
109
+ }
110
+ function jsonPointerPath(apiPath) {
111
+ return apiPath
112
+ .split("/")
113
+ .map((part) => part.replaceAll("~", "~0").replaceAll("/", "~1"))
114
+ .join("~1");
115
+ }
@@ -0,0 +1,4 @@
1
+ import type { Finding, FindingLocation, ProjectSignals } from "../schemas/types.js";
2
+ export declare function enrichFindingLocations(signals: ProjectSignals, findings: Finding[]): Finding[];
3
+ export declare function inferLocation(signals: ProjectSignals, finding: Pick<Finding, "affectedFile" | "ruleId" | "title" | "evidence" | "locationKey" | "subject">): FindingLocation | undefined;
4
+ //# sourceMappingURL=locations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locations.d.ts","sourceRoot":"","sources":["../../src/findings/locations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAWpF,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAa9F;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,IAAI,CACX,OAAO,EACP,cAAc,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAC7E,GACA,eAAe,GAAG,SAAS,CAmB7B"}
@@ -0,0 +1,117 @@
1
+ const headingByRule = [
2
+ { pattern: /quickstart/i, heading: /quick\s*start|quickstart|getting started/i },
3
+ { pattern: /install/i, heading: /install|installation|setup/i },
4
+ { pattern: /example|usage/i, heading: /example|usage|demo/i },
5
+ { pattern: /test/i, heading: /test|testing|verify|verification/i },
6
+ { pattern: /security|secret|privacy/i, heading: /security|secret|privacy/i },
7
+ { pattern: /architecture|repo map/i, heading: /architecture|structure|repo map/i }
8
+ ];
9
+ export function enrichFindingLocations(signals, findings) {
10
+ return findings.map((finding) => {
11
+ if (finding.location)
12
+ return finding;
13
+ const location = inferLocation(signals, finding);
14
+ return location
15
+ ? {
16
+ ...finding,
17
+ location,
18
+ locationKey: finding.locationKey ?? location.locationKey,
19
+ subject: finding.subject ?? location.subject
20
+ }
21
+ : finding;
22
+ });
23
+ }
24
+ export function inferLocation(signals, finding) {
25
+ const file = normalizeFile(finding.affectedFile ?? fileFromEvidence(finding.evidence));
26
+ if (!file || file === "n/a")
27
+ return undefined;
28
+ const content = signals.textByPath[file];
29
+ const sourceKind = sourceKindFor(finding.ruleId, file);
30
+ const line = sourceKind === "openapi"
31
+ ? openApiLine(content, finding)
32
+ : sourceKind === "mcp" || sourceKind === "task"
33
+ ? subjectLine(content, finding.subject)
34
+ : markdownLine(content, finding);
35
+ return {
36
+ file,
37
+ ...(line ? { startLine: line } : {}),
38
+ locationKey: finding.locationKey,
39
+ subject: finding.subject,
40
+ symbol: finding.subject,
41
+ sourceKind
42
+ };
43
+ }
44
+ function markdownLine(content, finding) {
45
+ if (!content)
46
+ return undefined;
47
+ const lines = content.split(/\r?\n/);
48
+ const haystack = `${finding.ruleId} ${finding.title}`;
49
+ const heading = headingByRule.find((entry) => entry.pattern.test(haystack))?.heading;
50
+ if (heading) {
51
+ const index = lines.findIndex((line) => /^#{1,6}\s+/.test(line) && heading.test(line));
52
+ if (index >= 0)
53
+ return index + 1;
54
+ }
55
+ return 1;
56
+ }
57
+ function openApiLine(content, finding) {
58
+ if (!content)
59
+ return undefined;
60
+ const subject = finding.subject ?? subjectFromEvidence(finding.evidence);
61
+ const match = subject?.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/i);
62
+ if (!match)
63
+ return 1;
64
+ const method = match[1]?.toLowerCase();
65
+ const apiPath = match[2];
66
+ const lines = content.split(/\r?\n/);
67
+ const pathIndex = lines.findIndex((line) => line.includes(`${apiPath}:`));
68
+ if (pathIndex < 0)
69
+ return 1;
70
+ const methodIndex = lines.findIndex((line, index) => index >= pathIndex &&
71
+ index < pathIndex + 30 &&
72
+ Boolean(method) &&
73
+ line.match(new RegExp(`^\\s*${method}:`)));
74
+ return methodIndex >= 0 ? methodIndex + 1 : pathIndex + 1;
75
+ }
76
+ function subjectLine(content, subject) {
77
+ if (!content)
78
+ return undefined;
79
+ if (!subject)
80
+ return 1;
81
+ const lines = content.split(/\r?\n/);
82
+ const index = lines.findIndex((line) => line.includes(subject));
83
+ return index >= 0 ? index + 1 : 1;
84
+ }
85
+ function sourceKindFor(ruleId, file) {
86
+ if (ruleId.startsWith("OPENAPI_") || /openapi|swagger/i.test(file))
87
+ return "openapi";
88
+ if (ruleId.startsWith("MCP_"))
89
+ return "mcp";
90
+ if (ruleId.startsWith("TASK_") || /agentlighthouse\.tasks/i.test(file))
91
+ return "task";
92
+ if (/\.md$|llms\.txt|AGENTS\.md|CLAUDE\.md/i.test(file))
93
+ return "markdown";
94
+ if (/package\.json|config/i.test(file))
95
+ return "config";
96
+ return "unknown";
97
+ }
98
+ function fileFromEvidence(evidence) {
99
+ const candidate = evidence[0]?.split(":")[0]?.trim();
100
+ if (!candidate || /\s/.test(candidate))
101
+ return undefined;
102
+ if (/^(AGENTS\.md|CLAUDE\.md|README\.md|llms\.txt|package\.json|agentlighthouse\.tasks\.ya?ml)$/i.test(candidate) ||
103
+ /\.(md|txt|json|ya?ml|ts|tsx|js|jsx|py|toml|rs|go|html)$/i.test(candidate)) {
104
+ return candidate;
105
+ }
106
+ return undefined;
107
+ }
108
+ function subjectFromEvidence(evidence) {
109
+ const match = evidence.join("; ").match(/\b(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s;(]+)/i);
110
+ return match ? `${match[1]?.toUpperCase()} ${match[2]}` : undefined;
111
+ }
112
+ function normalizeFile(file) {
113
+ return file
114
+ ?.trim()
115
+ .replaceAll("\\", "/")
116
+ .replace(/^\.\/+/, "");
117
+ }
@@ -0,0 +1,6 @@
1
+ import type { ArtifactGenerator, GeneratedArtifact, ProjectSignals } from "../schemas/types.js";
2
+ export declare class StarterArtifactGenerator implements ArtifactGenerator {
3
+ readonly id = "starter-artifacts";
4
+ generate(signals: ProjectSignals): GeneratedArtifact[];
5
+ }
6
+ //# sourceMappingURL=artifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../src/generators/artifacts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEhG,qBAAa,wBAAyB,YAAW,iBAAiB;IAChE,QAAQ,CAAC,EAAE,uBAAuB;IAElC,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,iBAAiB,EAAE;CA6BvD"}