@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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/analyzers/mcp.d.ts +8 -0
- package/dist/analyzers/mcp.d.ts.map +1 -0
- package/dist/analyzers/mcp.js +214 -0
- package/dist/analyzers/openapi.d.ts +7 -0
- package/dist/analyzers/openapi.d.ts.map +1 -0
- package/dist/analyzers/openapi.js +344 -0
- package/dist/analyzers/readiness.d.ts +8 -0
- package/dist/analyzers/readiness.d.ts.map +1 -0
- package/dist/analyzers/readiness.js +766 -0
- package/dist/analyzers/tasks.d.ts +3 -0
- package/dist/analyzers/tasks.d.ts.map +1 -0
- package/dist/analyzers/tasks.js +140 -0
- package/dist/changes/files.d.ts +5 -0
- package/dist/changes/files.d.ts.map +1 -0
- package/dist/changes/files.js +71 -0
- package/dist/comparison/compare.d.ts +14 -0
- package/dist/comparison/compare.d.ts.map +1 -0
- package/dist/comparison/compare.js +323 -0
- package/dist/config/profile.d.ts +16 -0
- package/dist/config/profile.d.ts.map +1 -0
- package/dist/config/profile.js +47 -0
- package/dist/detection/project.d.ts +4 -0
- package/dist/detection/project.d.ts.map +1 -0
- package/dist/detection/project.js +225 -0
- package/dist/findings/helpers.d.ts +36 -0
- package/dist/findings/helpers.d.ts.map +1 -0
- package/dist/findings/helpers.js +115 -0
- package/dist/findings/locations.d.ts +4 -0
- package/dist/findings/locations.d.ts.map +1 -0
- package/dist/findings/locations.js +117 -0
- package/dist/generators/artifacts.d.ts +6 -0
- package/dist/generators/artifacts.d.ts.map +1 -0
- package/dist/generators/artifacts.js +255 -0
- package/dist/index.d.ts +486 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +451 -0
- package/dist/probes/commands.d.ts +7 -0
- package/dist/probes/commands.d.ts.map +1 -0
- package/dist/probes/commands.js +198 -0
- package/dist/reporters/cli.d.ts +4 -0
- package/dist/reporters/cli.d.ts.map +1 -0
- package/dist/reporters/cli.js +42 -0
- package/dist/reporters/comparison.d.ts +13 -0
- package/dist/reporters/comparison.d.ts.map +1 -0
- package/dist/reporters/comparison.js +227 -0
- package/dist/reporters/github-summary.d.ts +4 -0
- package/dist/reporters/github-summary.d.ts.map +1 -0
- package/dist/reporters/github-summary.js +4 -0
- package/dist/reporters/json.d.ts +3 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +3 -0
- package/dist/reporters/markdown.d.ts +3 -0
- package/dist/reporters/markdown.d.ts.map +1 -0
- package/dist/reporters/markdown.js +146 -0
- package/dist/reporters/pr-summary.d.ts +8 -0
- package/dist/reporters/pr-summary.d.ts.map +1 -0
- package/dist/reporters/pr-summary.js +38 -0
- package/dist/reporters/sarif.d.ts +3 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +119 -0
- package/dist/reporters/shared.d.ts +8 -0
- package/dist/reporters/shared.d.ts.map +1 -0
- package/dist/reporters/shared.js +26 -0
- package/dist/scanners/filesystem.d.ts +6 -0
- package/dist/scanners/filesystem.d.ts.map +1 -0
- package/dist/scanners/filesystem.js +231 -0
- package/dist/schemas/types.d.ts +6652 -0
- package/dist/schemas/types.d.ts.map +1 -0
- package/dist/schemas/types.js +383 -0
- package/dist/scoring/calibration.d.ts +18 -0
- package/dist/scoring/calibration.d.ts.map +1 -0
- package/dist/scoring/calibration.js +231 -0
- package/dist/scoring/model.d.ts +21 -0
- package/dist/scoring/model.d.ts.map +1 -0
- package/dist/scoring/model.js +109 -0
- package/package.json +58 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { detectProject } from "../detection/project.js";
|
|
2
|
+
import { finding, hasUsefulMarkdownLinks, textIncludesAny } from "../findings/helpers.js";
|
|
3
|
+
export class ReadinessAnalyzer {
|
|
4
|
+
profile;
|
|
5
|
+
id = "readiness-analyzer";
|
|
6
|
+
constructor(profile = "default") {
|
|
7
|
+
this.profile = profile;
|
|
8
|
+
}
|
|
9
|
+
analyze(signals) {
|
|
10
|
+
return [
|
|
11
|
+
...agentInstructionFindings(signals),
|
|
12
|
+
...artifactQualityFindings(signals, this.profile),
|
|
13
|
+
...llmsFindings(signals),
|
|
14
|
+
...documentationFindings(signals),
|
|
15
|
+
...setupFindings(signals),
|
|
16
|
+
...apiFindings(signals, this.profile),
|
|
17
|
+
...mcpFindings(signals),
|
|
18
|
+
...securityFindings(signals),
|
|
19
|
+
...freshnessFindings(signals)
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function agentInstructionFindings(signals) {
|
|
24
|
+
const findings = [];
|
|
25
|
+
const agents = signals.textByPath["AGENTS.md"];
|
|
26
|
+
const claude = signals.textByPath["CLAUDE.md"];
|
|
27
|
+
if (!signals.artifacts["AGENTS.md"]?.exists) {
|
|
28
|
+
findings.push(finding({
|
|
29
|
+
id: "agent-instructions.missing-agents-md",
|
|
30
|
+
title: "Missing AGENTS.md",
|
|
31
|
+
severity: "high",
|
|
32
|
+
category: "agent_instructions",
|
|
33
|
+
description: "The repo has no AGENTS.md, so coding agents lack a stable project guide.",
|
|
34
|
+
evidence: ["AGENTS.md was not found at the repository root."],
|
|
35
|
+
recommendation: "Create AGENTS.md with setup, tests, architecture, conventions, and safety rules.",
|
|
36
|
+
affectedFile: "AGENTS.md",
|
|
37
|
+
suggestedFixType: "create_file"
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
if ((agents?.trim().length ?? 0) < 800) {
|
|
42
|
+
findings.push(finding({
|
|
43
|
+
id: "agent-instructions.agents-too-short",
|
|
44
|
+
title: "AGENTS.md is too short",
|
|
45
|
+
severity: "medium",
|
|
46
|
+
category: "agent_instructions",
|
|
47
|
+
description: "AGENTS.md exists but appears too brief to guide realistic agent work.",
|
|
48
|
+
evidence: [`AGENTS.md length is ${agents?.trim().length ?? 0} characters.`],
|
|
49
|
+
recommendation: "Expand AGENTS.md with commands, architecture boundaries, coding conventions, and safety guidance.",
|
|
50
|
+
affectedFile: "AGENTS.md",
|
|
51
|
+
suggestedFixType: "update_file"
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
for (const check of [
|
|
55
|
+
{
|
|
56
|
+
id: "agent-instructions.missing-setup",
|
|
57
|
+
title: "AGENTS.md does not mention setup commands",
|
|
58
|
+
terms: ["install", "setup", "pnpm", "npm", "yarn"],
|
|
59
|
+
recommendation: "Add the exact dependency installation and local setup commands."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "agent-instructions.missing-tests",
|
|
63
|
+
title: "AGENTS.md does not mention test commands",
|
|
64
|
+
terms: ["test", "vitest", "jest", "pytest"],
|
|
65
|
+
recommendation: "Add the exact commands agents should run before handing work back."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "agent-instructions.missing-conventions",
|
|
69
|
+
title: "AGENTS.md does not mention coding conventions",
|
|
70
|
+
terms: ["convention", "style", "lint", "format", "typescript", "architecture"],
|
|
71
|
+
recommendation: "Document project style, naming, and architecture expectations."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "agent-instructions.missing-safety",
|
|
75
|
+
title: "AGENTS.md does not mention safety or privacy constraints",
|
|
76
|
+
terms: ["secret", "privacy", "credential", "security", "sensitive"],
|
|
77
|
+
recommendation: "Tell agents how to handle secrets, credentials, private code, and external services."
|
|
78
|
+
}
|
|
79
|
+
]) {
|
|
80
|
+
if (!textIncludesAny(agents, check.terms)) {
|
|
81
|
+
findings.push(finding({
|
|
82
|
+
id: check.id,
|
|
83
|
+
title: check.title,
|
|
84
|
+
severity: "medium",
|
|
85
|
+
category: "agent_instructions",
|
|
86
|
+
description: "The main agent instruction file is missing a key operational section.",
|
|
87
|
+
evidence: [`AGENTS.md does not contain any of: ${check.terms.join(", ")}.`],
|
|
88
|
+
recommendation: check.recommendation,
|
|
89
|
+
affectedFile: "AGENTS.md",
|
|
90
|
+
suggestedFixType: "add_section"
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!signals.artifacts["CLAUDE.md"]?.exists) {
|
|
96
|
+
findings.push(finding({
|
|
97
|
+
id: "agent-instructions.missing-claude-md",
|
|
98
|
+
title: "Missing CLAUDE.md",
|
|
99
|
+
severity: "medium",
|
|
100
|
+
category: "agent_instructions",
|
|
101
|
+
description: "Claude Code users do not have a concise project memory file.",
|
|
102
|
+
evidence: ["CLAUDE.md was not found at the repository root."],
|
|
103
|
+
recommendation: "Add CLAUDE.md with concise workflow, boundaries, and testing expectations.",
|
|
104
|
+
affectedFile: "CLAUDE.md",
|
|
105
|
+
suggestedFixType: "create_file"
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
else if ((claude?.trim().length ?? 0) < 300 ||
|
|
109
|
+
textIncludesAny(claude, ["todo", "coming soon"])) {
|
|
110
|
+
findings.push(finding({
|
|
111
|
+
id: "agent-instructions.claude-vague",
|
|
112
|
+
title: "CLAUDE.md appears stale or too vague",
|
|
113
|
+
severity: "low",
|
|
114
|
+
category: "agent_instructions",
|
|
115
|
+
description: "CLAUDE.md exists but may not contain enough stable project memory.",
|
|
116
|
+
evidence: [`CLAUDE.md length is ${claude?.trim().length ?? 0} characters.`],
|
|
117
|
+
recommendation: "Refresh CLAUDE.md with current setup commands, product boundaries, and test expectations.",
|
|
118
|
+
affectedFile: "CLAUDE.md",
|
|
119
|
+
suggestedFixType: "update_file"
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
function llmsFindings(signals) {
|
|
125
|
+
const llms = signals.textByPath["llms.txt"];
|
|
126
|
+
if (!signals.artifacts["llms.txt"]?.exists) {
|
|
127
|
+
return [
|
|
128
|
+
finding({
|
|
129
|
+
id: "llms.missing",
|
|
130
|
+
title: "Missing llms.txt",
|
|
131
|
+
severity: "medium",
|
|
132
|
+
category: "agent_instructions",
|
|
133
|
+
description: "The project does not expose a compact, machine-readable map for LLMs and agents.",
|
|
134
|
+
evidence: ["llms.txt was not found at the repository root."],
|
|
135
|
+
recommendation: "Create llms.txt with links to README, docs, architecture, examples, and API references.",
|
|
136
|
+
affectedFile: "llms.txt",
|
|
137
|
+
suggestedFixType: "create_file"
|
|
138
|
+
})
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
const findings = [];
|
|
142
|
+
if (!hasUsefulMarkdownLinks(llms)) {
|
|
143
|
+
findings.push(finding({
|
|
144
|
+
id: "llms.no-useful-links",
|
|
145
|
+
title: "llms.txt has no useful links",
|
|
146
|
+
severity: "medium",
|
|
147
|
+
category: "agent_instructions",
|
|
148
|
+
description: "llms.txt exists but does not appear to point agents at important project context.",
|
|
149
|
+
evidence: ["No Markdown links, URLs, or root-relative links were detected."],
|
|
150
|
+
recommendation: "Add links to README, docs, architecture, scoring model, examples, and API references.",
|
|
151
|
+
affectedFile: "llms.txt",
|
|
152
|
+
suggestedFixType: "update_file"
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
const missingReferences = findLocalReferences(llms).filter((reference) => !signals.scannedFiles.includes(reference.replace(/^\//, "")));
|
|
156
|
+
if (missingReferences.length > 0) {
|
|
157
|
+
findings.push(finding({
|
|
158
|
+
id: "llms.references-missing-files",
|
|
159
|
+
title: "llms.txt references missing files",
|
|
160
|
+
severity: "low",
|
|
161
|
+
category: "freshness_and_consistency",
|
|
162
|
+
description: "Some local links in llms.txt do not resolve to scanned files.",
|
|
163
|
+
evidence: missingReferences.slice(0, 5),
|
|
164
|
+
recommendation: "Update or remove stale llms.txt links.",
|
|
165
|
+
affectedFile: "llms.txt",
|
|
166
|
+
suggestedFixType: "update_file"
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
}
|
|
171
|
+
function documentationFindings(signals) {
|
|
172
|
+
const findings = [];
|
|
173
|
+
const readme = signals.textByPath["README.md"];
|
|
174
|
+
if (!signals.artifacts["README.md"]?.exists) {
|
|
175
|
+
findings.push(finding({
|
|
176
|
+
id: "docs.missing-readme",
|
|
177
|
+
title: "Missing README.md",
|
|
178
|
+
severity: "high",
|
|
179
|
+
category: "documentation",
|
|
180
|
+
description: "Agents and humans lack the most common project entry point.",
|
|
181
|
+
evidence: ["README.md was not found at the repository root."],
|
|
182
|
+
recommendation: "Create a README with purpose, installation, quickstart, examples, and development commands.",
|
|
183
|
+
affectedFile: "README.md",
|
|
184
|
+
suggestedFixType: "create_file"
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
for (const check of [
|
|
189
|
+
{
|
|
190
|
+
id: "docs.readme-no-quickstart",
|
|
191
|
+
title: "README.md has no quickstart section",
|
|
192
|
+
terms: ["quickstart", "quick start", "getting started"],
|
|
193
|
+
recommendation: "Add a quickstart that gets a new user to a working command quickly."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "docs.readme-no-install",
|
|
197
|
+
title: "README.md has no installation instructions",
|
|
198
|
+
terms: ["install", "pnpm", "npm", "yarn", "pip"],
|
|
199
|
+
recommendation: "Add installation commands and prerequisites."
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "docs.readme-no-examples",
|
|
203
|
+
title: "README.md has no examples",
|
|
204
|
+
terms: ["example", "usage", "demo"],
|
|
205
|
+
recommendation: "Add concrete examples showing expected usage and output."
|
|
206
|
+
}
|
|
207
|
+
]) {
|
|
208
|
+
if (!textIncludesAny(readme, check.terms)) {
|
|
209
|
+
findings.push(finding({
|
|
210
|
+
id: check.id,
|
|
211
|
+
title: check.title,
|
|
212
|
+
severity: "medium",
|
|
213
|
+
category: "documentation",
|
|
214
|
+
description: "The README is missing a section agents commonly need to orient themselves.",
|
|
215
|
+
evidence: [`README.md does not contain any of: ${check.terms.join(", ")}.`],
|
|
216
|
+
recommendation: check.recommendation,
|
|
217
|
+
affectedFile: "README.md",
|
|
218
|
+
suggestedFixType: "add_section"
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!signals.scannedFiles.some((file) => file.startsWith("docs/"))) {
|
|
224
|
+
findings.push(finding({
|
|
225
|
+
id: "docs.directory-missing",
|
|
226
|
+
title: "Docs directory missing",
|
|
227
|
+
severity: "low",
|
|
228
|
+
category: "documentation",
|
|
229
|
+
description: "No docs directory was detected for deeper project context.",
|
|
230
|
+
evidence: ["No scanned files were under docs/."],
|
|
231
|
+
recommendation: "Add docs/ for architecture, development, API, and operational context.",
|
|
232
|
+
affectedFile: "docs/",
|
|
233
|
+
suggestedFixType: "create_file"
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
else if (signals.docsMarkdownFiles.length === 0) {
|
|
237
|
+
findings.push(finding({
|
|
238
|
+
id: "docs.no-markdown",
|
|
239
|
+
title: "Docs exist but no Markdown files are discoverable",
|
|
240
|
+
severity: "medium",
|
|
241
|
+
category: "documentation",
|
|
242
|
+
description: "Agents work best when documentation source is available in text formats.",
|
|
243
|
+
evidence: ["docs/ exists but no .md or .mdx files were scanned."],
|
|
244
|
+
recommendation: "Add Markdown documentation or ensure docs source is not ignored.",
|
|
245
|
+
affectedFile: "docs/",
|
|
246
|
+
suggestedFixType: "add_section"
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
return findings;
|
|
250
|
+
}
|
|
251
|
+
function setupFindings(signals) {
|
|
252
|
+
const detected = detectProject(signals);
|
|
253
|
+
const packageJson = signals.packageJson;
|
|
254
|
+
if (!packageJson) {
|
|
255
|
+
if (detected.type === "node_javascript" || detected.type === "node_typescript") {
|
|
256
|
+
return [
|
|
257
|
+
finding({
|
|
258
|
+
id: "setup.package-json-missing",
|
|
259
|
+
title: "package.json missing for detected Node project",
|
|
260
|
+
severity: "high",
|
|
261
|
+
category: "setup_and_tests",
|
|
262
|
+
description: "Node projects need package.json scripts so agents can discover local workflows.",
|
|
263
|
+
evidence: detected.evidence,
|
|
264
|
+
recommendation: "Add package.json with scripts for install, test, lint, typecheck, build, and local development.",
|
|
265
|
+
affectedFile: "package.json",
|
|
266
|
+
suggestedFixType: "review_manually"
|
|
267
|
+
})
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
if (detected.type === "docs_only" ||
|
|
271
|
+
detected.type === "python" ||
|
|
272
|
+
detected.type === "rust" ||
|
|
273
|
+
detected.type === "go") {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
const findings = [];
|
|
279
|
+
const scripts = packageJson.scripts;
|
|
280
|
+
if (Object.keys(scripts).length === 0) {
|
|
281
|
+
findings.push(finding({
|
|
282
|
+
id: "setup.package-json-no-scripts",
|
|
283
|
+
title: "package.json has no scripts",
|
|
284
|
+
severity: "high",
|
|
285
|
+
category: "setup_and_tests",
|
|
286
|
+
description: "Agents cannot discover standard local workflows from package.json.",
|
|
287
|
+
evidence: ["package.json scripts object is empty."],
|
|
288
|
+
recommendation: "Add scripts for test, lint, typecheck, build, and local development.",
|
|
289
|
+
affectedFile: "package.json",
|
|
290
|
+
suggestedFixType: "add_script"
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
for (const scriptName of ["test", "lint", "typecheck"]) {
|
|
294
|
+
if (!scripts[scriptName]) {
|
|
295
|
+
findings.push(finding({
|
|
296
|
+
id: `setup.missing-${scriptName}-script`,
|
|
297
|
+
title: `No ${scriptName} script in package.json`,
|
|
298
|
+
severity: scriptName === "test" ? "high" : "medium",
|
|
299
|
+
category: "setup_and_tests",
|
|
300
|
+
description: `Agents cannot reliably run ${scriptName} without a package script.`,
|
|
301
|
+
evidence: [`package.json scripts does not include "${scriptName}".`],
|
|
302
|
+
recommendation: `Add a package.json "${scriptName}" script or document the equivalent command clearly.`,
|
|
303
|
+
affectedFile: "package.json",
|
|
304
|
+
suggestedFixType: "add_script"
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const readme = signals.textByPath["README.md"];
|
|
309
|
+
if (readme) {
|
|
310
|
+
const mentionedScripts = [
|
|
311
|
+
...readme.matchAll(/^\s*(?:[$>]\s*)?(?:pnpm|npm run|yarn)\s+([a-zA-Z0-9:_-]+)/gm)
|
|
312
|
+
].map((match) => match[1]);
|
|
313
|
+
const missingScripts = [...new Set(mentionedScripts)].filter((script) => script &&
|
|
314
|
+
!script.startsWith("-") &&
|
|
315
|
+
!["install", "add", "create"].includes(script) &&
|
|
316
|
+
!scripts[script]);
|
|
317
|
+
if (missingScripts.length > 0) {
|
|
318
|
+
findings.push(finding({
|
|
319
|
+
id: "setup.readme-mentions-missing-scripts",
|
|
320
|
+
title: "README mentions commands not present in package.json",
|
|
321
|
+
severity: "low",
|
|
322
|
+
category: "freshness_and_consistency",
|
|
323
|
+
description: "Documentation and executable scripts appear inconsistent.",
|
|
324
|
+
evidence: missingScripts.map((script) => `README mentions ${script}, but package.json has no matching script.`),
|
|
325
|
+
recommendation: "Update README commands or add the missing package scripts.",
|
|
326
|
+
affectedFile: "README.md",
|
|
327
|
+
suggestedFixType: "update_file"
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return findings;
|
|
332
|
+
}
|
|
333
|
+
function apiFindings(signals, profile) {
|
|
334
|
+
const detected = detectProject(signals);
|
|
335
|
+
const relevantOpenApiFiles = signals.openApiFiles.filter((file) => !file.startsWith("examples/"));
|
|
336
|
+
if (relevantOpenApiFiles.length === 0) {
|
|
337
|
+
const severity = detected.type === "openapi_project" || profile === "api" ? "high" : "info";
|
|
338
|
+
return [
|
|
339
|
+
finding({
|
|
340
|
+
id: "api.openapi-not-detected",
|
|
341
|
+
title: "OpenAPI file not detected",
|
|
342
|
+
severity,
|
|
343
|
+
category: "api_schema",
|
|
344
|
+
description: "No OpenAPI schema was found. This may be fine for projects without an HTTP API.",
|
|
345
|
+
evidence: ["No openapi.* or swagger.* file was scanned."],
|
|
346
|
+
recommendation: "For API products, publish an OpenAPI spec with operation descriptions and examples.",
|
|
347
|
+
suggestedFixType: "review_manually"
|
|
348
|
+
})
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
const findings = [
|
|
352
|
+
finding({
|
|
353
|
+
id: "api.openapi-detected",
|
|
354
|
+
title: "OpenAPI file detected",
|
|
355
|
+
severity: "info",
|
|
356
|
+
category: "api_schema",
|
|
357
|
+
description: "The scanner found an API schema that agents can use.",
|
|
358
|
+
evidence: relevantOpenApiFiles,
|
|
359
|
+
recommendation: "Keep API descriptions, examples, and auth details current.",
|
|
360
|
+
affectedFile: relevantOpenApiFiles[0],
|
|
361
|
+
suggestedFixType: "none"
|
|
362
|
+
})
|
|
363
|
+
];
|
|
364
|
+
const openApiText = relevantOpenApiFiles.map((file) => signals.textByPath[file] ?? "").join("\n");
|
|
365
|
+
const hasNearbyExample = signals.scannedFiles.some((file) => /examples?\//i.test(file)) ||
|
|
366
|
+
/\bexamples?:/i.test(openApiText);
|
|
367
|
+
if (!hasNearbyExample) {
|
|
368
|
+
findings.push(finding({
|
|
369
|
+
id: "api.openapi-no-nearby-examples",
|
|
370
|
+
title: "OpenAPI exists but no examples are nearby",
|
|
371
|
+
severity: "medium",
|
|
372
|
+
category: "api_schema",
|
|
373
|
+
description: "Agents need request and response examples in addition to schemas.",
|
|
374
|
+
evidence: [`OpenAPI files: ${relevantOpenApiFiles.join(", ")}`],
|
|
375
|
+
recommendation: "Add examples near the API spec or link examples from the API documentation.",
|
|
376
|
+
affectedFile: relevantOpenApiFiles[0],
|
|
377
|
+
suggestedFixType: "add_example"
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
if (!/description:\s*.{20,}/i.test(openApiText) &&
|
|
381
|
+
!/"description"\s*:\s*".{20,}"/i.test(openApiText)) {
|
|
382
|
+
findings.push(finding({
|
|
383
|
+
id: "api.openapi-thin-descriptions",
|
|
384
|
+
title: "OpenAPI operation descriptions appear thin",
|
|
385
|
+
severity: "medium",
|
|
386
|
+
category: "api_schema",
|
|
387
|
+
description: "The OpenAPI schema may not provide enough semantic guidance for agents.",
|
|
388
|
+
evidence: ["No operation description longer than 20 characters was detected."],
|
|
389
|
+
recommendation: "Add meaningful operation descriptions, auth notes, and representative examples.",
|
|
390
|
+
affectedFile: relevantOpenApiFiles[0],
|
|
391
|
+
suggestedFixType: "update_file"
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
return findings;
|
|
395
|
+
}
|
|
396
|
+
function mcpFindings(signals) {
|
|
397
|
+
const detected = detectProject(signals);
|
|
398
|
+
if (signals.mcpFiles.length === 0) {
|
|
399
|
+
return [
|
|
400
|
+
finding({
|
|
401
|
+
id: "mcp.not-evaluated",
|
|
402
|
+
title: "MCP readiness could not be evaluated yet",
|
|
403
|
+
severity: detected.type === "mcp_project" ? "high" : "info",
|
|
404
|
+
category: "mcp_tools",
|
|
405
|
+
description: "No MCP server/config/package signal was detected.",
|
|
406
|
+
evidence: ["No file or package name matching MCP was scanned."],
|
|
407
|
+
recommendation: "If this project exposes MCP tools, include server files and clear tool descriptions.",
|
|
408
|
+
suggestedFixType: "review_manually"
|
|
409
|
+
})
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
const mcpText = signals.mcpFiles.map((file) => signals.textByPath[file] ?? file).join("\n");
|
|
413
|
+
const findings = [
|
|
414
|
+
finding({
|
|
415
|
+
id: "mcp.detected",
|
|
416
|
+
title: "MCP-related files or packages detected",
|
|
417
|
+
severity: "info",
|
|
418
|
+
category: "mcp_tools",
|
|
419
|
+
description: "MCP signals were found and can be evaluated by future deeper analyzers.",
|
|
420
|
+
evidence: signals.mcpFiles.slice(0, 5),
|
|
421
|
+
recommendation: "Ensure each MCP tool has a clear name, description, input schema, and safety guidance.",
|
|
422
|
+
affectedFile: signals.mcpFiles[0],
|
|
423
|
+
suggestedFixType: "review_manually"
|
|
424
|
+
})
|
|
425
|
+
];
|
|
426
|
+
if (!/description[\s\S]{10,}/i.test(mcpText)) {
|
|
427
|
+
findings.push(finding({
|
|
428
|
+
id: "mcp.tool-descriptions-thin",
|
|
429
|
+
title: "MCP tool descriptions appear missing or too short",
|
|
430
|
+
severity: "medium",
|
|
431
|
+
category: "mcp_tools",
|
|
432
|
+
description: "Detected MCP-related files do not expose clear tool descriptions.",
|
|
433
|
+
evidence: [
|
|
434
|
+
"No description-like text longer than 10 characters was detected in MCP signals."
|
|
435
|
+
],
|
|
436
|
+
recommendation: "Add concise, action-oriented descriptions to MCP tools and schemas.",
|
|
437
|
+
affectedFile: signals.mcpFiles[0],
|
|
438
|
+
suggestedFixType: "update_file"
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
return findings;
|
|
442
|
+
}
|
|
443
|
+
function artifactQualityFindings(signals, profile) {
|
|
444
|
+
const findings = [];
|
|
445
|
+
const checks = [
|
|
446
|
+
{
|
|
447
|
+
file: "AGENTS.md",
|
|
448
|
+
label: "AGENTS.md",
|
|
449
|
+
optional: false,
|
|
450
|
+
required: [
|
|
451
|
+
["architecture map", ["architecture", "packages/", "apps/", "src/"]],
|
|
452
|
+
["common mistakes or avoid-list", ["avoid", "do not", "don't", "common mistake"]],
|
|
453
|
+
["generated-file warnings", ["generated", "dist", "build output", "do not edit"]]
|
|
454
|
+
]
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
file: "CLAUDE.md",
|
|
458
|
+
label: "CLAUDE.md",
|
|
459
|
+
optional: false,
|
|
460
|
+
required: [
|
|
461
|
+
["testing expectations", ["test", "testing", "vitest", "pytest"]],
|
|
462
|
+
["product boundaries", ["non-goal", "boundary", "not a", "do not build"]],
|
|
463
|
+
["development workflow", ["workflow", "pnpm", "npm", "setup"]]
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
file: "README.md",
|
|
468
|
+
label: "README.md",
|
|
469
|
+
optional: false,
|
|
470
|
+
required: [
|
|
471
|
+
[
|
|
472
|
+
"clear test command",
|
|
473
|
+
["test", "pnpm test", "npm test", "pytest", "cargo test", "go test"]
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
"architecture or repo map",
|
|
477
|
+
["architecture", "repo structure", "packages/", "apps/", "src/"]
|
|
478
|
+
]
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
file: ".github/copilot-instructions.md",
|
|
483
|
+
label: "GitHub Copilot instructions",
|
|
484
|
+
optional: true,
|
|
485
|
+
required: [
|
|
486
|
+
["setup commands", ["install", "setup", "pnpm", "npm", "pip"]],
|
|
487
|
+
["test commands", ["test", "pytest", "vitest", "jest"]],
|
|
488
|
+
["security/privacy guidance", ["secret", "privacy", "credential", "sensitive"]]
|
|
489
|
+
]
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
file: ".cursor/rules",
|
|
493
|
+
label: "Cursor rules",
|
|
494
|
+
optional: true,
|
|
495
|
+
required: [
|
|
496
|
+
["coding conventions", ["convention", "style", "format", "naming"]],
|
|
497
|
+
["architecture map", ["architecture", "packages/", "apps/", "src/"]],
|
|
498
|
+
["generated-file warnings", ["generated", "dist", "build output", "do not edit"]]
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
];
|
|
502
|
+
for (const artifact of checks) {
|
|
503
|
+
const exists = signals.artifacts[artifact.file]?.exists;
|
|
504
|
+
if (!exists) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const content = signals.textByPath[artifact.file] ??
|
|
508
|
+
Object.entries(signals.textByPath)
|
|
509
|
+
.filter(([file]) => file.startsWith(`${artifact.file}/`))
|
|
510
|
+
.map(([, value]) => value)
|
|
511
|
+
.join("\n");
|
|
512
|
+
for (const [label, terms] of artifact.required) {
|
|
513
|
+
if (!textIncludesAny(content, terms)) {
|
|
514
|
+
findings.push(finding({
|
|
515
|
+
id: `artifact-quality.${artifact.file.replaceAll("/", "-").replaceAll(".", "")}.missing-${slug(label)}`,
|
|
516
|
+
title: `${artifact.label} exists, but does not include ${label}`,
|
|
517
|
+
severity: artifact.optional ? "low" : "medium",
|
|
518
|
+
category: "agent_instructions",
|
|
519
|
+
description: "A detected agent-facing artifact is missing specific guidance agents need for reliable work.",
|
|
520
|
+
evidence: [`${artifact.label} does not contain any of: ${terms.join(", ")}.`],
|
|
521
|
+
recommendation: `Add ${label} to ${artifact.label}.`,
|
|
522
|
+
affectedFile: artifact.file,
|
|
523
|
+
suggestedFixType: "add_section"
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
findings.push(...strongSignalFindings(signals, profile));
|
|
529
|
+
return findings;
|
|
530
|
+
}
|
|
531
|
+
function strongSignalFindings(signals, profile) {
|
|
532
|
+
const findings = [];
|
|
533
|
+
const agents = signals.textByPath["AGENTS.md"];
|
|
534
|
+
const readme = signals.textByPath["README.md"];
|
|
535
|
+
const llms = signals.textByPath["llms.txt"];
|
|
536
|
+
const benchmarkContent = signals.benchmarkFiles
|
|
537
|
+
.map((file) => signals.textByPath[file] ?? "")
|
|
538
|
+
.join("\n");
|
|
539
|
+
if (agents && fencedCommandCount(agents) < 3) {
|
|
540
|
+
findings.push(finding({
|
|
541
|
+
id: "artifact-quality.agents-missing-command-blocks",
|
|
542
|
+
title: "AGENTS.md has too few fenced command examples",
|
|
543
|
+
severity: "low",
|
|
544
|
+
category: "agent_instructions",
|
|
545
|
+
description: "Agent instructions are easier to execute when setup, test, and build commands are shown as fenced command blocks.",
|
|
546
|
+
evidence: [`Detected ${fencedCommandCount(agents)} fenced shell command block(s).`],
|
|
547
|
+
recommendation: "Add fenced command examples for install, test, lint/typecheck, and build workflows.",
|
|
548
|
+
affectedFile: "AGENTS.md",
|
|
549
|
+
suggestedFixType: "update_file"
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
if (readme && !hasVerificationStep(readme)) {
|
|
553
|
+
findings.push(finding({
|
|
554
|
+
id: "artifact-quality.readme-missing-verification-step",
|
|
555
|
+
title: "README has installation guidance but no verification step",
|
|
556
|
+
severity: "low",
|
|
557
|
+
category: "documentation",
|
|
558
|
+
description: "Agents need a quick command to verify the project works after installation.",
|
|
559
|
+
evidence: [
|
|
560
|
+
"README does not show an obvious test, build, healthcheck, or smoke-test step after installation."
|
|
561
|
+
],
|
|
562
|
+
recommendation: "Add a short verification step such as running tests, typecheck, build, or a health command.",
|
|
563
|
+
affectedFile: "README.md",
|
|
564
|
+
suggestedFixType: "add_section"
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
567
|
+
if (readme && signals.packageJson && !commandsMatchScripts(readme, signals.packageJson.scripts)) {
|
|
568
|
+
findings.push(finding({
|
|
569
|
+
id: "artifact-quality.readme-commands-not-grounded-in-scripts",
|
|
570
|
+
title: "README commands are not clearly grounded in package.json scripts",
|
|
571
|
+
severity: "low",
|
|
572
|
+
category: "freshness_and_consistency",
|
|
573
|
+
description: "Commands are more trustworthy when README examples match executable project scripts.",
|
|
574
|
+
evidence: [
|
|
575
|
+
"No fenced README command references a package.json script such as test, lint, typecheck, dev, or build."
|
|
576
|
+
],
|
|
577
|
+
recommendation: "Show package-manager commands that map directly to package.json scripts.",
|
|
578
|
+
affectedFile: "README.md",
|
|
579
|
+
suggestedFixType: "update_file"
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
if (llms && findLocalReferences(llms).length < 4) {
|
|
583
|
+
findings.push(finding({
|
|
584
|
+
id: "artifact-quality.llms-too-few-project-links",
|
|
585
|
+
title: "llms.txt links to too few concrete project files",
|
|
586
|
+
severity: "low",
|
|
587
|
+
category: "agent_instructions",
|
|
588
|
+
description: "A useful llms.txt should route agents to README, architecture, development, examples, and task workflows.",
|
|
589
|
+
evidence: [`Detected ${findLocalReferences(llms).length} local link(s).`],
|
|
590
|
+
recommendation: "Add links to concrete docs, source entry points, examples, and benchmark tasks.",
|
|
591
|
+
affectedFile: "llms.txt",
|
|
592
|
+
suggestedFixType: "update_file"
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
if (benchmarkContent && !/(success_criteria|successCriteria):\s*\n\s*-/i.test(benchmarkContent)) {
|
|
596
|
+
findings.push(finding({
|
|
597
|
+
id: "artifact-quality.benchmarks-not-verifiable",
|
|
598
|
+
title: "Benchmark tasks are not verifiable",
|
|
599
|
+
severity: "medium",
|
|
600
|
+
category: "task_benchmarks",
|
|
601
|
+
description: "Benchmark tasks need explicit success criteria so agent completion can be evaluated.",
|
|
602
|
+
evidence: ["No success criteria list was detected."],
|
|
603
|
+
recommendation: "Add success criteria to each benchmark task.",
|
|
604
|
+
affectedFile: signals.benchmarkFiles[0],
|
|
605
|
+
suggestedFixType: "update_file"
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
if ((profile === "devtool" || profile === "api") &&
|
|
609
|
+
readme &&
|
|
610
|
+
!/troubleshoot|debug|common issue|faq/i.test(readme)) {
|
|
611
|
+
findings.push(finding({
|
|
612
|
+
id: "artifact-quality.readme-missing-troubleshooting",
|
|
613
|
+
title: "README lacks troubleshooting guidance",
|
|
614
|
+
severity: "low",
|
|
615
|
+
category: "documentation",
|
|
616
|
+
description: "Developer-tool and API projects benefit from troubleshooting notes because agents often need to recover from local setup failures.",
|
|
617
|
+
evidence: ["No troubleshooting, debug, FAQ, or common-issue section was detected."],
|
|
618
|
+
recommendation: "Add a short troubleshooting section with common setup and test failure fixes.",
|
|
619
|
+
affectedFile: "README.md",
|
|
620
|
+
suggestedFixType: "add_section"
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
if (agents && !/owner|maintain|review|approval|responsible/i.test(agents)) {
|
|
624
|
+
findings.push(finding({
|
|
625
|
+
id: "artifact-quality.agents-missing-ownership-notes",
|
|
626
|
+
title: "AGENTS.md lacks ownership or maintenance notes",
|
|
627
|
+
severity: "low",
|
|
628
|
+
category: "agent_instructions",
|
|
629
|
+
description: "Agents need to know when to preserve ownership boundaries, ask for review, or avoid changing maintained areas.",
|
|
630
|
+
evidence: [
|
|
631
|
+
"No ownership, maintainer, review, approval, or responsibility language was detected."
|
|
632
|
+
],
|
|
633
|
+
recommendation: "Add maintenance and ownership notes for sensitive modules or review expectations.",
|
|
634
|
+
affectedFile: "AGENTS.md",
|
|
635
|
+
suggestedFixType: "add_section"
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
return findings;
|
|
639
|
+
}
|
|
640
|
+
function securityFindings(signals) {
|
|
641
|
+
const findings = [];
|
|
642
|
+
const agents = signals.textByPath["AGENTS.md"];
|
|
643
|
+
if (!signals.artifacts[".agentlighthouseignore"]?.exists) {
|
|
644
|
+
findings.push(finding({
|
|
645
|
+
id: "security.missing-agentlighthouseignore",
|
|
646
|
+
title: "Missing .agentlighthouseignore",
|
|
647
|
+
severity: "medium",
|
|
648
|
+
category: "security_and_privacy",
|
|
649
|
+
description: "Scans should explicitly exclude generated, private, and secret-bearing paths.",
|
|
650
|
+
evidence: [".agentlighthouseignore was not found at the repository root."],
|
|
651
|
+
recommendation: "Add .agentlighthouseignore with node_modules, build outputs, env files, secrets, and vendor paths.",
|
|
652
|
+
affectedFile: ".agentlighthouseignore",
|
|
653
|
+
suggestedFixType: "create_file"
|
|
654
|
+
}));
|
|
655
|
+
}
|
|
656
|
+
if (!textIncludesAny(agents, [
|
|
657
|
+
"secret",
|
|
658
|
+
"privacy",
|
|
659
|
+
"credential",
|
|
660
|
+
"security",
|
|
661
|
+
"sensitive",
|
|
662
|
+
"external llm"
|
|
663
|
+
])) {
|
|
664
|
+
findings.push(finding({
|
|
665
|
+
id: "security.agent-secret-guidance-missing",
|
|
666
|
+
title: "Instructions do not tell agents how to handle secrets",
|
|
667
|
+
severity: "medium",
|
|
668
|
+
category: "security_and_privacy",
|
|
669
|
+
description: "Agent-facing instructions should state how to avoid exposing secrets or private data.",
|
|
670
|
+
evidence: ["No secret/privacy guidance was detected in AGENTS.md."],
|
|
671
|
+
recommendation: "Add a security section explaining secret handling and external LLM constraints.",
|
|
672
|
+
affectedFile: "AGENTS.md",
|
|
673
|
+
suggestedFixType: "add_section"
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
const secretMatches = Object.entries(signals.textByPath)
|
|
677
|
+
.filter(([file]) => /^(docs|examples|README|AGENTS|CLAUDE|llms)/.test(file))
|
|
678
|
+
.flatMap(([file, content]) => {
|
|
679
|
+
const matches = content.match(/(?:api[_-]?key|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_-]{16,}/gi);
|
|
680
|
+
return matches?.map((match) => `${file}: ${maskSecret(match)}`) ?? [];
|
|
681
|
+
});
|
|
682
|
+
if (secretMatches.length > 0) {
|
|
683
|
+
findings.push(finding({
|
|
684
|
+
id: "security.secret-looking-strings",
|
|
685
|
+
title: "Potential secret-looking strings in docs or examples",
|
|
686
|
+
severity: "critical",
|
|
687
|
+
category: "security_and_privacy",
|
|
688
|
+
description: "Documentation or examples contain strings that look like credentials.",
|
|
689
|
+
evidence: secretMatches.slice(0, 5),
|
|
690
|
+
recommendation: "Replace real-looking credentials with clearly fake placeholders.",
|
|
691
|
+
suggestedFixType: "review_manually"
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
return findings;
|
|
695
|
+
}
|
|
696
|
+
function freshnessFindings(signals) {
|
|
697
|
+
const staleMatches = Object.entries(signals.textByPath)
|
|
698
|
+
.filter(([file]) => file.endsWith(".md") || file.endsWith(".txt"))
|
|
699
|
+
.flatMap(([file, content]) => {
|
|
700
|
+
const lines = content.split(/\r?\n/);
|
|
701
|
+
return lines.flatMap((line, index) => {
|
|
702
|
+
const proseLine = stripInlineCode(line);
|
|
703
|
+
return /\b(TODO|coming soon|legacy|deprecated|old)\b/i.test(proseLine) &&
|
|
704
|
+
!/migration|replace|instead|new/i.test(proseLine)
|
|
705
|
+
? [`${file}:${index + 1}: ${line.trim().slice(0, 120)}`]
|
|
706
|
+
: [];
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
if (staleMatches.length === 0) {
|
|
710
|
+
return [];
|
|
711
|
+
}
|
|
712
|
+
return [
|
|
713
|
+
finding({
|
|
714
|
+
id: "freshness.deprecated-or-todo-terms",
|
|
715
|
+
title: "Docs contain TODO/deprecated-looking terms without migration guidance",
|
|
716
|
+
severity: "low",
|
|
717
|
+
category: "freshness_and_consistency",
|
|
718
|
+
description: "Stale markers can confuse agents unless they include replacement guidance.",
|
|
719
|
+
evidence: staleMatches.slice(0, 8),
|
|
720
|
+
recommendation: "Resolve TODOs or add explicit migration/replacement guidance.",
|
|
721
|
+
suggestedFixType: "update_file"
|
|
722
|
+
})
|
|
723
|
+
];
|
|
724
|
+
}
|
|
725
|
+
function stripInlineCode(value) {
|
|
726
|
+
return value.replace(/`[^`]*`/g, "");
|
|
727
|
+
}
|
|
728
|
+
function findLocalReferences(text) {
|
|
729
|
+
if (!text) {
|
|
730
|
+
return [];
|
|
731
|
+
}
|
|
732
|
+
return [...text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)]
|
|
733
|
+
.map((match) => match[1])
|
|
734
|
+
.filter((reference) => Boolean(reference))
|
|
735
|
+
.filter((reference) => !reference.startsWith("http") && !reference.startsWith("#"))
|
|
736
|
+
.map((reference) => reference.replace(/^\.?\//, "").split("#")[0] ?? "");
|
|
737
|
+
}
|
|
738
|
+
function maskSecret(value) {
|
|
739
|
+
return value.length <= 12 ? "[masked]" : `${value.slice(0, 8)}...[masked]`;
|
|
740
|
+
}
|
|
741
|
+
function slug(value) {
|
|
742
|
+
return value
|
|
743
|
+
.toLowerCase()
|
|
744
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
745
|
+
.replace(/^-|-$/g, "");
|
|
746
|
+
}
|
|
747
|
+
function fencedCommandCount(text) {
|
|
748
|
+
return [...text.matchAll(/```(?:bash|sh|shell|zsh)?\n([\s\S]*?)```/gi)].filter((match) => /\b(pnpm|npm|yarn|bun|pip|poetry|cargo|go)\s+[a-z0-9:_-]+/i.test(match[1] ?? "")).length;
|
|
749
|
+
}
|
|
750
|
+
function hasVerificationStep(text) {
|
|
751
|
+
return (/\b(pnpm|npm|yarn|bun)\s+(test|build|typecheck|lint)\b/i.test(text) ||
|
|
752
|
+
/\b(pytest|cargo test|go test)\b/i.test(text) ||
|
|
753
|
+
/\bverify|verification|smoke test|healthcheck\b/i.test(text));
|
|
754
|
+
}
|
|
755
|
+
function commandsMatchScripts(text, scripts) {
|
|
756
|
+
const scriptNames = Object.keys(scripts);
|
|
757
|
+
if (scriptNames.length === 0)
|
|
758
|
+
return false;
|
|
759
|
+
return scriptNames.some((script) => {
|
|
760
|
+
const escaped = escapeRegex(script);
|
|
761
|
+
return new RegExp(`\\b(pnpm|npm run|npm|yarn|bun)\\s+${escaped}\\b`, "i").test(text);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
function escapeRegex(value) {
|
|
765
|
+
return value.replace(/[|\\{}()[\]^$+?.*]/g, "\\$&");
|
|
766
|
+
}
|