@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentLighthouse contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @agentlighthouse/core
|
|
2
|
+
|
|
3
|
+
Core scanner, schemas, scoring, comparison, and reporters for AgentLighthouse.
|
|
4
|
+
|
|
5
|
+
This package is intended for tool authors who want to embed AgentLighthouse's local-first agent-readiness analysis. The public alpha API is intentionally centered on the root export:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
scanProject,
|
|
10
|
+
compareScanResults,
|
|
11
|
+
renderMarkdownReport,
|
|
12
|
+
renderSarifReport
|
|
13
|
+
} from "@agentlighthouse/core";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The API is alpha. See `docs/SCHEMA_STABILITY.md` in the repository before depending on report schemas or finding fingerprints.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding, McpAnalysis, ProjectSignals } from "../schemas/types.js";
|
|
2
|
+
interface McpAnalyzerResult {
|
|
3
|
+
analysis: McpAnalysis;
|
|
4
|
+
findings: Finding[];
|
|
5
|
+
}
|
|
6
|
+
export declare function analyzeMcp(signals: ProjectSignals): McpAnalyzerResult;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/analyzers/mcp.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAmB,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAkCjG,UAAU,iBAAiB;IACzB,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,iBAAiB,CAsIrE"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { finding } from "../findings/helpers.js";
|
|
2
|
+
const ambiguousNames = new Set([
|
|
3
|
+
"run",
|
|
4
|
+
"query",
|
|
5
|
+
"create",
|
|
6
|
+
"update",
|
|
7
|
+
"delete",
|
|
8
|
+
"tool",
|
|
9
|
+
"call",
|
|
10
|
+
"exec"
|
|
11
|
+
]);
|
|
12
|
+
const destructiveWords = [
|
|
13
|
+
"delete",
|
|
14
|
+
"remove",
|
|
15
|
+
"write",
|
|
16
|
+
"update",
|
|
17
|
+
"create",
|
|
18
|
+
"drop",
|
|
19
|
+
"truncate",
|
|
20
|
+
"revoke"
|
|
21
|
+
];
|
|
22
|
+
const privacyWords = [
|
|
23
|
+
"secret",
|
|
24
|
+
"token",
|
|
25
|
+
"auth",
|
|
26
|
+
"private",
|
|
27
|
+
"user",
|
|
28
|
+
"email",
|
|
29
|
+
"database",
|
|
30
|
+
"filesystem",
|
|
31
|
+
"network"
|
|
32
|
+
];
|
|
33
|
+
export function analyzeMcp(signals) {
|
|
34
|
+
const files = detectMcpFiles(signals);
|
|
35
|
+
const tools = files.flatMap((file) => extractTools(file, signals.textByPath[file] ?? ""));
|
|
36
|
+
const ambiguousTools = tools.filter((tool) => ambiguousNames.has(tool.name.toLowerCase()));
|
|
37
|
+
const destructiveTools = tools.filter((tool) => tool.destructive);
|
|
38
|
+
const privacySensitiveTools = tools.filter((tool) => tool.privacySensitive);
|
|
39
|
+
const weakTools = tools.filter((tool) => tool.weak);
|
|
40
|
+
const findings = [];
|
|
41
|
+
if (files.length > 0 && tools.length === 0) {
|
|
42
|
+
findings.push(finding({
|
|
43
|
+
id: "MCP_TOOL_DEFINITIONS_NOT_PARSED",
|
|
44
|
+
title: "MCP project detected but tool definitions were not parsed",
|
|
45
|
+
severity: "medium",
|
|
46
|
+
category: "mcp_tools",
|
|
47
|
+
description: "MCP files or dependencies were detected, but static analysis could not find clear tool registrations.",
|
|
48
|
+
evidence: files.slice(0, 8),
|
|
49
|
+
recommendation: "Use explicit registerTool/server.tool definitions with names, descriptions, schemas, and examples.",
|
|
50
|
+
agentFailureMode: "A coding agent may know the project exposes MCP but be unable to infer which tools exist or when to use them.",
|
|
51
|
+
fixExample: "Register tools with specific names and descriptions, for example registerTool('list_workspace_documents', { description, inputSchema }).",
|
|
52
|
+
suggestedFixType: "update_file"
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
pushToolFinding(findings, {
|
|
56
|
+
id: "MCP_TOOL_NAME_AMBIGUOUS",
|
|
57
|
+
title: "MCP tool names are ambiguous",
|
|
58
|
+
severity: "medium",
|
|
59
|
+
tools: ambiguousTools,
|
|
60
|
+
recommendation: "Rename generic tools so the action and resource are obvious.",
|
|
61
|
+
agentFailureMode: "A coding agent may choose a generic tool like run or query for the wrong workflow.",
|
|
62
|
+
fixExample: "Use names such as search_docs, create_workspace_ticket, or revoke_api_key."
|
|
63
|
+
});
|
|
64
|
+
pushToolFinding(findings, {
|
|
65
|
+
id: "MCP_TOOL_DESCRIPTION_MISSING",
|
|
66
|
+
title: "MCP tools are missing descriptions",
|
|
67
|
+
severity: "high",
|
|
68
|
+
tools: tools.filter((tool) => !tool.description),
|
|
69
|
+
recommendation: "Add a description explaining when to use the tool and when not to use it.",
|
|
70
|
+
agentFailureMode: "A coding agent may call a tool from its name alone and miss preconditions or side effects.",
|
|
71
|
+
fixExample: "Description: 'Search public docs by keyword. Do not use for private customer data.'"
|
|
72
|
+
});
|
|
73
|
+
pushToolFinding(findings, {
|
|
74
|
+
id: "MCP_TOOL_DESCRIPTION_SHALLOW",
|
|
75
|
+
title: "MCP tool descriptions are too shallow",
|
|
76
|
+
severity: "medium",
|
|
77
|
+
tools: tools.filter((tool) => tool.description && tool.description.length < 40),
|
|
78
|
+
recommendation: "Expand tool descriptions with intent, constraints, and safe usage guidance.",
|
|
79
|
+
agentFailureMode: "A coding agent may not distinguish similar tools without usage context.",
|
|
80
|
+
fixExample: "Mention input expectations, side effects, auth requirements, and error behavior."
|
|
81
|
+
});
|
|
82
|
+
pushToolFinding(findings, {
|
|
83
|
+
id: "MCP_TOOL_INPUT_SCHEMA_MISSING",
|
|
84
|
+
title: "MCP tools are missing input schemas",
|
|
85
|
+
severity: "high",
|
|
86
|
+
tools: tools.filter((tool) => !tool.hasInputSchema),
|
|
87
|
+
recommendation: "Add structured input schemas with required fields and descriptions.",
|
|
88
|
+
agentFailureMode: "A coding agent may pass malformed arguments or omit required identifiers.",
|
|
89
|
+
fixExample: "Use a zod/object schema with workspace_id and query fields plus descriptions."
|
|
90
|
+
});
|
|
91
|
+
pushToolFinding(findings, {
|
|
92
|
+
id: "MCP_TOOL_EXAMPLE_MISSING",
|
|
93
|
+
title: "MCP tools lack examples or usage notes",
|
|
94
|
+
severity: "low",
|
|
95
|
+
tools: tools.filter((tool) => !tool.hasExamples),
|
|
96
|
+
recommendation: "Add examples or usage notes for common calls.",
|
|
97
|
+
agentFailureMode: "A coding agent may not know the expected argument shape or output semantics.",
|
|
98
|
+
fixExample: "Include an example invocation showing realistic placeholder arguments."
|
|
99
|
+
});
|
|
100
|
+
pushToolFinding(findings, {
|
|
101
|
+
id: "MCP_TOOL_DESTRUCTIVE_ACTION_UNMARKED",
|
|
102
|
+
title: "Destructive MCP tools are not clearly marked",
|
|
103
|
+
severity: "high",
|
|
104
|
+
tools: destructiveTools.filter((tool) => !/danger|destructive|irreversible|confirm|permission/i.test(tool.description ?? "")),
|
|
105
|
+
recommendation: "Mark destructive tools and document confirmation, permissions, and safe testing behavior.",
|
|
106
|
+
agentFailureMode: "A coding agent may call a write/delete tool without realizing it changes external state.",
|
|
107
|
+
fixExample: "State that the tool mutates data and requires explicit user approval outside test fixtures."
|
|
108
|
+
});
|
|
109
|
+
pushToolFinding(findings, {
|
|
110
|
+
id: "MCP_TOOL_AUTH_PRIVACY_UNCLEAR",
|
|
111
|
+
title: "Privacy-sensitive MCP tools lack auth/privacy guidance",
|
|
112
|
+
severity: "medium",
|
|
113
|
+
tools: privacySensitiveTools.filter((tool) => !/auth|permission|private|secret|token|privacy/i.test(tool.description ?? "")),
|
|
114
|
+
recommendation: "Document credentials, permissions, and private-data handling for sensitive tools.",
|
|
115
|
+
agentFailureMode: "A coding agent may expose private data or call a tool without required auth context.",
|
|
116
|
+
fixExample: "Mention required token scopes and whether arguments or outputs may contain private data."
|
|
117
|
+
});
|
|
118
|
+
pushToolFinding(findings, {
|
|
119
|
+
id: "MCP_TOOL_ERROR_BEHAVIOR_UNCLEAR",
|
|
120
|
+
title: "MCP tool error behavior is unclear",
|
|
121
|
+
severity: "low",
|
|
122
|
+
tools: tools.filter((tool) => !/error|fail|not found|unauthorized|retry/i.test(tool.description ?? "")),
|
|
123
|
+
recommendation: "Explain common errors and whether retry or user follow-up is appropriate.",
|
|
124
|
+
agentFailureMode: "A coding agent may retry unsafe operations or fail silently when a tool returns an error.",
|
|
125
|
+
fixExample: "Document not-found, unauthorized, rate-limit, and validation failures."
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
analysis: {
|
|
129
|
+
detected: files.length > 0,
|
|
130
|
+
files,
|
|
131
|
+
toolCount: tools.length,
|
|
132
|
+
toolsWithSchemas: tools.filter((tool) => tool.hasInputSchema).length,
|
|
133
|
+
toolsWithExamples: tools.filter((tool) => tool.hasExamples).length,
|
|
134
|
+
ambiguousTools: ambiguousTools.map((tool) => `${tool.file}: ${tool.name}`),
|
|
135
|
+
destructiveTools: destructiveTools.map((tool) => `${tool.file}: ${tool.name}`),
|
|
136
|
+
privacySensitiveTools: privacySensitiveTools.map((tool) => `${tool.file}: ${tool.name}`),
|
|
137
|
+
weakTools: weakTools.map((tool) => `${tool.file}: ${tool.name}`)
|
|
138
|
+
},
|
|
139
|
+
findings
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function detectMcpFiles(signals) {
|
|
143
|
+
const dependencies = new Set([
|
|
144
|
+
...(signals.packageJson?.dependencies ?? []),
|
|
145
|
+
...(signals.packageJson?.devDependencies ?? [])
|
|
146
|
+
]);
|
|
147
|
+
const hasMcpPackage = [...dependencies].some((dependency) => /modelcontextprotocol|mcp/i.test(dependency));
|
|
148
|
+
const patternFiles = Object.entries(signals.textByPath)
|
|
149
|
+
.filter(([file, content]) => hasMcpPackage &&
|
|
150
|
+
!file.startsWith("docs/") &&
|
|
151
|
+
/registerTool|server\.tool|new McpServer|@modelcontextprotocol/i.test(content))
|
|
152
|
+
.map(([file]) => file);
|
|
153
|
+
return [
|
|
154
|
+
...new Set([...signals.mcpFiles.filter((file) => signals.textByPath[file]), ...patternFiles])
|
|
155
|
+
].sort();
|
|
156
|
+
}
|
|
157
|
+
function extractTools(file, content) {
|
|
158
|
+
const tools = [];
|
|
159
|
+
const registrationPattern = /(?:registerTool|server\.tool|\.tool)\(\s*["'`]([^"'`]+)["'`]\s*,\s*\{([\s\S]*?)\n\}\s*\);/g;
|
|
160
|
+
for (const match of content.matchAll(registrationPattern)) {
|
|
161
|
+
const name = match[1] ?? "unknown_tool";
|
|
162
|
+
const block = match[2] ?? "";
|
|
163
|
+
const description = extractDescription(block);
|
|
164
|
+
const haystack = `${name} ${description ?? ""} ${block}`.toLowerCase();
|
|
165
|
+
const destructive = destructiveWords.some((word) => haystack.includes(word));
|
|
166
|
+
const privacySensitive = privacyWords.some((word) => haystack.includes(word));
|
|
167
|
+
const hasInputSchema = /inputSchema|schema|z\.object|properties\s*:|required\s*:/i.test(block);
|
|
168
|
+
const hasExamples = /example|usage|sample/i.test(block);
|
|
169
|
+
const riskReasons = [
|
|
170
|
+
ambiguousNames.has(name.toLowerCase()) ? "ambiguous name" : undefined,
|
|
171
|
+
!description ? "missing description" : undefined,
|
|
172
|
+
description && description.length < 40 ? "shallow description" : undefined,
|
|
173
|
+
!hasInputSchema ? "missing input schema" : undefined,
|
|
174
|
+
destructive ? "destructive or mutating behavior" : undefined,
|
|
175
|
+
privacySensitive ? "privacy/auth sensitive behavior" : undefined
|
|
176
|
+
].filter((reason) => Boolean(reason));
|
|
177
|
+
tools.push({
|
|
178
|
+
name,
|
|
179
|
+
file,
|
|
180
|
+
description,
|
|
181
|
+
hasInputSchema,
|
|
182
|
+
hasExamples,
|
|
183
|
+
destructive,
|
|
184
|
+
privacySensitive,
|
|
185
|
+
weak: riskReasons.length > 0,
|
|
186
|
+
riskReasons
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return tools;
|
|
190
|
+
}
|
|
191
|
+
function extractDescription(block) {
|
|
192
|
+
const propertyMatch = block.match(/description\s*:\s*["'`]([^"'`]+)["'`]/i);
|
|
193
|
+
if (propertyMatch?.[1])
|
|
194
|
+
return propertyMatch[1];
|
|
195
|
+
const stringArgument = block.match(/^\s*["'`]([^"'`]+)["'`]/);
|
|
196
|
+
return stringArgument?.[1];
|
|
197
|
+
}
|
|
198
|
+
function pushToolFinding(findings, input) {
|
|
199
|
+
if (input.tools.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
findings.push(finding({
|
|
202
|
+
id: input.id,
|
|
203
|
+
title: input.title,
|
|
204
|
+
severity: input.severity,
|
|
205
|
+
category: "mcp_tools",
|
|
206
|
+
description: `${input.tools.length} MCP tool(s) need stronger agent-facing metadata.`,
|
|
207
|
+
evidence: input.tools.slice(0, 8).map((tool) => `${tool.file}: ${tool.name}`),
|
|
208
|
+
recommendation: input.recommendation,
|
|
209
|
+
agentFailureMode: input.agentFailureMode,
|
|
210
|
+
fixExample: input.fixExample,
|
|
211
|
+
affectedFile: input.tools[0]?.file,
|
|
212
|
+
suggestedFixType: "update_file"
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ApiAnalysis, Finding, ProjectSignals } from "../schemas/types.js";
|
|
2
|
+
export interface OpenApiAnalyzerResult {
|
|
3
|
+
analysis: ApiAnalysis;
|
|
4
|
+
findings: Finding[];
|
|
5
|
+
}
|
|
6
|
+
export declare function analyzeOpenApi(signals: ProjectSignals): OpenApiAnalyzerResult;
|
|
7
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/analyzers/openapi.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA0BhF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,qBAAqB,CAyC7E"}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { finding } from "../findings/helpers.js";
|
|
3
|
+
const httpMethods = new Set(["get", "post", "put", "patch", "delete"]);
|
|
4
|
+
const errorStatusCodes = ["400", "401", "403", "404", "409", "429", "500"];
|
|
5
|
+
const destructiveWords = ["delete", "cancel", "refund", "revoke", "close", "remove"];
|
|
6
|
+
const genericOperationNames = new Set([
|
|
7
|
+
"get",
|
|
8
|
+
"list",
|
|
9
|
+
"create",
|
|
10
|
+
"update",
|
|
11
|
+
"delete",
|
|
12
|
+
"run",
|
|
13
|
+
"query",
|
|
14
|
+
"execute",
|
|
15
|
+
"submit"
|
|
16
|
+
]);
|
|
17
|
+
export function analyzeOpenApi(signals) {
|
|
18
|
+
const specs = signals.openApiFiles
|
|
19
|
+
.map((file) => ({ file, spec: parseSpec(signals.textByPath[file]) }))
|
|
20
|
+
.filter((entry) => Boolean(entry.spec));
|
|
21
|
+
const operations = specs.flatMap(({ file, spec }) => collectOperations(file, spec));
|
|
22
|
+
const authSchemes = specs.flatMap(({ spec }) => securitySchemes(spec));
|
|
23
|
+
const operationLabels = operations.map(labelOperation);
|
|
24
|
+
const destructiveOperations = operations.filter(isDestructiveOperation).map(labelOperation);
|
|
25
|
+
const operationsWithExamples = operations.filter((operation) => hasRequestExample(operation.operation) || hasResponseExample(operation.operation)).length;
|
|
26
|
+
const operationsMissingDescriptions = operations.filter(({ operation }) => weakText(stringValue(operation.description), 32)).length;
|
|
27
|
+
const weakOperations = operations.filter(isWeakOperation).map(labelOperation);
|
|
28
|
+
const highRiskOperations = operations
|
|
29
|
+
.filter((operation) => isDestructiveOperation(operation) || !hasErrorResponses(operation.operation))
|
|
30
|
+
.map(labelOperation);
|
|
31
|
+
const findings = [];
|
|
32
|
+
findings.push(...specLevelFindings(specs));
|
|
33
|
+
findings.push(...operationQualityFindings(operations));
|
|
34
|
+
return {
|
|
35
|
+
analysis: {
|
|
36
|
+
specFiles: specs.map((entry) => entry.file),
|
|
37
|
+
operationCount: operations.length,
|
|
38
|
+
operationsWithExamples,
|
|
39
|
+
operationsMissingDescriptions,
|
|
40
|
+
destructiveOperations,
|
|
41
|
+
authSchemes,
|
|
42
|
+
weakOperations: weakOperations.slice(0, 20),
|
|
43
|
+
highRiskOperations: highRiskOperations.slice(0, 20)
|
|
44
|
+
},
|
|
45
|
+
findings: operationLabels.length === 0 && specs.length > 0
|
|
46
|
+
? [...findings, noOperationsFinding(specs[0]?.file)]
|
|
47
|
+
: findings
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function parseSpec(content) {
|
|
51
|
+
if (!content)
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = content.trim().startsWith("{")
|
|
55
|
+
? JSON.parse(content)
|
|
56
|
+
: parseYaml(content);
|
|
57
|
+
return asObject(parsed);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function collectOperations(file, spec) {
|
|
64
|
+
const paths = asObject(spec.paths);
|
|
65
|
+
if (!paths)
|
|
66
|
+
return [];
|
|
67
|
+
const operations = [];
|
|
68
|
+
for (const [apiPath, pathItem] of Object.entries(paths)) {
|
|
69
|
+
const pathObject = asObject(pathItem);
|
|
70
|
+
if (!pathObject)
|
|
71
|
+
continue;
|
|
72
|
+
for (const [method, operation] of Object.entries(pathObject)) {
|
|
73
|
+
if (!httpMethods.has(method.toLowerCase()))
|
|
74
|
+
continue;
|
|
75
|
+
const operationObject = asObject(operation);
|
|
76
|
+
if (!operationObject)
|
|
77
|
+
continue;
|
|
78
|
+
operations.push({
|
|
79
|
+
file,
|
|
80
|
+
method: method.toUpperCase(),
|
|
81
|
+
path: apiPath,
|
|
82
|
+
operation: operationObject
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return operations;
|
|
87
|
+
}
|
|
88
|
+
function specLevelFindings(specs) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
for (const { file, spec } of specs) {
|
|
91
|
+
const info = asObject(spec.info);
|
|
92
|
+
const description = stringValue(info?.description);
|
|
93
|
+
const authSchemes = securitySchemes(spec);
|
|
94
|
+
const fullText = JSON.stringify(spec).toLowerCase();
|
|
95
|
+
if (weakText(description, 40)) {
|
|
96
|
+
findings.push(finding({
|
|
97
|
+
id: "OPENAPI_WEAK_SPEC_DESCRIPTION",
|
|
98
|
+
title: "OpenAPI spec description is too thin for agents",
|
|
99
|
+
severity: "medium",
|
|
100
|
+
category: "api_schema",
|
|
101
|
+
description: "The API-level description does not explain product concepts, auth expectations, or common workflows.",
|
|
102
|
+
evidence: [`${file}: info.description is missing or shorter than 40 characters.`],
|
|
103
|
+
recommendation: "Add a concise overview of the API domain, common workflows, auth model, and safe usage constraints.",
|
|
104
|
+
agentFailureMode: "A coding agent may jump straight to endpoints without understanding product nouns, authentication, or workflow ordering.",
|
|
105
|
+
fixExample: "Describe the main resources, required authentication, common create/list/update flows, and where to find examples.",
|
|
106
|
+
affectedFile: file,
|
|
107
|
+
suggestedFixType: "update_file"
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
if (authSchemes.length === 0 &&
|
|
111
|
+
!/(api key|bearer|oauth|authorization|authentication)/i.test(fullText)) {
|
|
112
|
+
findings.push(finding({
|
|
113
|
+
id: "OPENAPI_AUTH_UNCLEAR",
|
|
114
|
+
title: "OpenAPI authentication is unclear",
|
|
115
|
+
severity: "high",
|
|
116
|
+
category: "api_schema",
|
|
117
|
+
description: "No security scheme or clear authentication guidance was detected.",
|
|
118
|
+
evidence: [
|
|
119
|
+
`${file}: components.securitySchemes is missing and auth hints were not found.`
|
|
120
|
+
],
|
|
121
|
+
recommendation: "Document the auth scheme, required headers, scopes, and placeholder-safe example credentials.",
|
|
122
|
+
agentFailureMode: "A coding agent may generate client code without the required Authorization header or may invent unsafe credential handling.",
|
|
123
|
+
fixExample: "Add an HTTP bearer or API key security scheme and a request example using EXAMPLE_API_KEY.",
|
|
124
|
+
affectedFile: file,
|
|
125
|
+
suggestedFixType: "update_file"
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(spec.servers) && !stringValue(spec.host)) {
|
|
129
|
+
findings.push(finding({
|
|
130
|
+
id: "OPENAPI_SERVER_URL_MISSING",
|
|
131
|
+
title: "OpenAPI server URL is missing",
|
|
132
|
+
severity: "low",
|
|
133
|
+
category: "api_schema",
|
|
134
|
+
description: "Agents need a base URL signal to generate usable client examples.",
|
|
135
|
+
evidence: [`${file}: servers is missing.`],
|
|
136
|
+
recommendation: "Add production and sandbox server URLs, or explain how agents should configure the base URL.",
|
|
137
|
+
agentFailureMode: "A coding agent may invent a base URL or hardcode the wrong host.",
|
|
138
|
+
fixExample: "Add servers: [{ url: 'https://api.example.com/v1' }].",
|
|
139
|
+
affectedFile: file,
|
|
140
|
+
suggestedFixType: "update_file"
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
function operationQualityFindings(operations) {
|
|
147
|
+
const findings = [];
|
|
148
|
+
const missingOperationIds = operations.filter((operation) => weakOperationId(operation.operation));
|
|
149
|
+
const weakDescriptions = operations.filter(({ operation }) => weakText(stringValue(operation.description), 32));
|
|
150
|
+
const missingRequestExamples = operations.filter((operation) => ["POST", "PUT", "PATCH"].includes(operation.method) && !hasRequestExample(operation.operation));
|
|
151
|
+
const missingResponseExamples = operations.filter((operation) => !hasResponseExample(operation.operation));
|
|
152
|
+
const missingErrorResponses = operations.filter((operation) => !hasErrorResponses(operation.operation));
|
|
153
|
+
const ambiguousNames = operations.filter((operation) => ambiguousOperationName(operation.operation));
|
|
154
|
+
const unmarkedDestructive = operations.filter((operation) => isDestructiveOperation(operation) && !destructiveOperationMarked(operation.operation));
|
|
155
|
+
const paginationUnclear = operations.filter((operation) => likelyListOperation(operation) &&
|
|
156
|
+
!operationMentions(operation.operation, /(page|cursor|limit|offset|next)/i));
|
|
157
|
+
const rateLimitUnclear = operations.length > 0 &&
|
|
158
|
+
!operations.some((operation) => operationMentions(operation.operation, /(rate limit|429|too many requests|retry-after)/i));
|
|
159
|
+
pushAggregate(findings, {
|
|
160
|
+
id: "OPENAPI_MISSING_OPERATION_ID",
|
|
161
|
+
title: "OpenAPI operations have weak or missing operationIds",
|
|
162
|
+
severity: "medium",
|
|
163
|
+
records: missingOperationIds,
|
|
164
|
+
recommendation: "Give every operation a stable, specific operationId such as createWorkspaceInvite or listCustomerInvoices.",
|
|
165
|
+
agentFailureMode: "A coding agent may generate confusing client methods or call the wrong endpoint when operation IDs are missing or generic.",
|
|
166
|
+
fixExample: "Use verb+noun names that distinguish similar operations, for example revokeApiKey instead of delete."
|
|
167
|
+
});
|
|
168
|
+
pushAggregate(findings, {
|
|
169
|
+
id: "OPENAPI_WEAK_OPERATION_DESCRIPTION",
|
|
170
|
+
title: "OpenAPI operations have weak descriptions",
|
|
171
|
+
severity: "medium",
|
|
172
|
+
records: weakDescriptions,
|
|
173
|
+
recommendation: "Add operation descriptions explaining when to use the endpoint, required context, side effects, and recovery behavior.",
|
|
174
|
+
agentFailureMode: "A coding agent may choose the wrong operation because summaries alone do not explain intent or preconditions.",
|
|
175
|
+
fixExample: "Add descriptions that mention required IDs, auth scope, side effects, and common failure handling."
|
|
176
|
+
});
|
|
177
|
+
pushAggregate(findings, {
|
|
178
|
+
id: "OPENAPI_MISSING_REQUEST_EXAMPLE",
|
|
179
|
+
title: "Write operations lack request examples",
|
|
180
|
+
severity: "medium",
|
|
181
|
+
records: missingRequestExamples,
|
|
182
|
+
recommendation: "Add request examples for create/update operations using realistic placeholder values.",
|
|
183
|
+
agentFailureMode: "A coding agent may send malformed payloads or omit required fields because no concrete request shape is shown.",
|
|
184
|
+
fixExample: "Add an example body with workspace_id, customer_id, and idempotency_key where relevant."
|
|
185
|
+
});
|
|
186
|
+
pushAggregate(findings, {
|
|
187
|
+
id: "OPENAPI_MISSING_RESPONSE_EXAMPLE",
|
|
188
|
+
title: "Operations lack response examples",
|
|
189
|
+
severity: "low",
|
|
190
|
+
records: missingResponseExamples,
|
|
191
|
+
recommendation: "Add response examples for successful and important error cases.",
|
|
192
|
+
agentFailureMode: "A coding agent may write incorrect parsing code because expected response shapes are not exemplified.",
|
|
193
|
+
fixExample: "Add 200/201 response examples with IDs, timestamps, pagination cursors, and nested objects."
|
|
194
|
+
});
|
|
195
|
+
pushAggregate(findings, {
|
|
196
|
+
id: "OPENAPI_MISSING_ERROR_RESPONSES",
|
|
197
|
+
title: "Operations lack common error responses",
|
|
198
|
+
severity: "medium",
|
|
199
|
+
records: missingErrorResponses,
|
|
200
|
+
recommendation: "Document likely 400/401/403/404/409/429/500 responses and how callers should recover.",
|
|
201
|
+
agentFailureMode: "A coding agent may generate happy-path-only integrations with no auth, retry, or validation recovery.",
|
|
202
|
+
fixExample: "Add 401, 404, 409, and 429 responses with short descriptions and example error payloads."
|
|
203
|
+
});
|
|
204
|
+
pushAggregate(findings, {
|
|
205
|
+
id: "OPENAPI_AMBIGUOUS_OPERATION_NAME",
|
|
206
|
+
title: "OpenAPI operation names are ambiguous",
|
|
207
|
+
severity: "low",
|
|
208
|
+
records: ambiguousNames,
|
|
209
|
+
recommendation: "Rename generic operationIds so similar endpoints are distinguishable to code generators and agents.",
|
|
210
|
+
agentFailureMode: "A coding agent may map multiple endpoints to vague client methods such as run, query, or update.",
|
|
211
|
+
fixExample: "Prefer syncCatalog, searchDocuments, or updateWorkspaceMember over generic names."
|
|
212
|
+
});
|
|
213
|
+
pushAggregate(findings, {
|
|
214
|
+
id: "OPENAPI_DESTRUCTIVE_OPERATION_UNMARKED",
|
|
215
|
+
title: "Destructive API operations are not clearly marked",
|
|
216
|
+
severity: "high",
|
|
217
|
+
records: unmarkedDestructive,
|
|
218
|
+
recommendation: "Mark destructive operations with permission, confirmation, irreversibility, and recovery guidance.",
|
|
219
|
+
agentFailureMode: "A coding agent may call a destructive endpoint during testing without understanding consequences.",
|
|
220
|
+
fixExample: "State whether delete/cancel/revoke is irreversible and include sandbox-safe examples."
|
|
221
|
+
});
|
|
222
|
+
pushAggregate(findings, {
|
|
223
|
+
id: "OPENAPI_PAGINATION_UNCLEAR",
|
|
224
|
+
title: "List operations do not explain pagination",
|
|
225
|
+
severity: "low",
|
|
226
|
+
records: paginationUnclear,
|
|
227
|
+
recommendation: "Document pagination parameters, cursors, limits, and response fields.",
|
|
228
|
+
agentFailureMode: "A coding agent may fetch only the first page or invent unsupported pagination parameters.",
|
|
229
|
+
fixExample: "Describe limit and cursor parameters and include a response example with next_cursor."
|
|
230
|
+
});
|
|
231
|
+
if (rateLimitUnclear) {
|
|
232
|
+
findings.push(finding({
|
|
233
|
+
id: "OPENAPI_RATE_LIMIT_UNCLEAR",
|
|
234
|
+
title: "OpenAPI rate-limit behavior is unclear",
|
|
235
|
+
severity: "low",
|
|
236
|
+
category: "api_schema",
|
|
237
|
+
description: "No 429, Retry-After, or rate-limit guidance was detected.",
|
|
238
|
+
evidence: ["No operation mentions 429, Retry-After, or rate limits."],
|
|
239
|
+
recommendation: "Document rate limits and retry behavior for generated clients.",
|
|
240
|
+
agentFailureMode: "A coding agent may create brittle retry loops or ignore throttling responses.",
|
|
241
|
+
fixExample: "Add a 429 response with Retry-After guidance and an example error payload.",
|
|
242
|
+
suggestedFixType: "update_file"
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
return findings;
|
|
246
|
+
}
|
|
247
|
+
function pushAggregate(findings, input) {
|
|
248
|
+
if (input.records.length === 0)
|
|
249
|
+
return;
|
|
250
|
+
findings.push(finding({
|
|
251
|
+
id: input.id,
|
|
252
|
+
title: input.title,
|
|
253
|
+
severity: input.severity,
|
|
254
|
+
category: "api_schema",
|
|
255
|
+
description: `${input.records.length} operation(s) need stronger agent-facing API documentation.`,
|
|
256
|
+
evidence: input.records.slice(0, 8).map(labelOperation),
|
|
257
|
+
recommendation: input.recommendation,
|
|
258
|
+
agentFailureMode: input.agentFailureMode,
|
|
259
|
+
fixExample: input.fixExample,
|
|
260
|
+
affectedFile: input.records[0]?.file,
|
|
261
|
+
suggestedFixType: "update_file"
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
function noOperationsFinding(file) {
|
|
265
|
+
return finding({
|
|
266
|
+
id: "OPENAPI_NO_OPERATIONS",
|
|
267
|
+
title: "OpenAPI spec has no analyzable operations",
|
|
268
|
+
severity: "high",
|
|
269
|
+
category: "api_schema",
|
|
270
|
+
description: "The spec was detected but no HTTP path operations could be parsed.",
|
|
271
|
+
evidence: [file ?? "OpenAPI file detected."],
|
|
272
|
+
recommendation: "Add OpenAPI path operations or fix the spec structure so tools can parse it.",
|
|
273
|
+
agentFailureMode: "A coding agent cannot discover API workflows from a spec without operations.",
|
|
274
|
+
fixExample: "Add paths with get/post/patch/delete operations and operationIds.",
|
|
275
|
+
affectedFile: file,
|
|
276
|
+
suggestedFixType: "update_file"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function securitySchemes(spec) {
|
|
280
|
+
const components = asObject(spec.components);
|
|
281
|
+
const schemes = asObject(components?.securitySchemes);
|
|
282
|
+
return schemes ? Object.keys(schemes) : [];
|
|
283
|
+
}
|
|
284
|
+
function weakOperationId(operation) {
|
|
285
|
+
const operationId = stringValue(operation.operationId);
|
|
286
|
+
return (!operationId || operationId.length < 5 || genericOperationNames.has(operationId.toLowerCase()));
|
|
287
|
+
}
|
|
288
|
+
function ambiguousOperationName(operation) {
|
|
289
|
+
const operationId = stringValue(operation.operationId);
|
|
290
|
+
if (!operationId)
|
|
291
|
+
return false;
|
|
292
|
+
return genericOperationNames.has(operationId.toLowerCase()) || !/[A-Z_-]/.test(operationId);
|
|
293
|
+
}
|
|
294
|
+
function isWeakOperation(record) {
|
|
295
|
+
return (weakOperationId(record.operation) ||
|
|
296
|
+
weakText(stringValue(record.operation.description), 32) ||
|
|
297
|
+
!hasResponseExample(record.operation) ||
|
|
298
|
+
!hasErrorResponses(record.operation));
|
|
299
|
+
}
|
|
300
|
+
function isDestructiveOperation(record) {
|
|
301
|
+
const haystack = `${record.method} ${record.path} ${stringValue(record.operation.operationId)} ${stringValue(record.operation.summary)} ${stringValue(record.operation.description)}`.toLowerCase();
|
|
302
|
+
return record.method === "DELETE" || destructiveWords.some((word) => haystack.includes(word));
|
|
303
|
+
}
|
|
304
|
+
function destructiveOperationMarked(operation) {
|
|
305
|
+
return operationMentions(operation, /(irreversible|destructive|permission|confirm|cannot be undone|sandbox|danger)/i);
|
|
306
|
+
}
|
|
307
|
+
function likelyListOperation(record) {
|
|
308
|
+
const haystack = `${record.method} ${record.path} ${stringValue(record.operation.operationId)} ${stringValue(record.operation.summary)}`.toLowerCase();
|
|
309
|
+
return record.method === "GET" && /(list|search|\/\{[^}]+\}$)/i.test(haystack);
|
|
310
|
+
}
|
|
311
|
+
function hasRequestExample(operation) {
|
|
312
|
+
return JSON.stringify(operation.requestBody ?? "")
|
|
313
|
+
.toLowerCase()
|
|
314
|
+
.includes("example");
|
|
315
|
+
}
|
|
316
|
+
function hasResponseExample(operation) {
|
|
317
|
+
return JSON.stringify(operation.responses ?? "")
|
|
318
|
+
.toLowerCase()
|
|
319
|
+
.includes("example");
|
|
320
|
+
}
|
|
321
|
+
function hasErrorResponses(operation) {
|
|
322
|
+
const responses = asObject(operation.responses);
|
|
323
|
+
if (!responses)
|
|
324
|
+
return false;
|
|
325
|
+
return errorStatusCodes.some((code) => Object.prototype.hasOwnProperty.call(responses, code));
|
|
326
|
+
}
|
|
327
|
+
function operationMentions(operation, pattern) {
|
|
328
|
+
return pattern.test(JSON.stringify(operation));
|
|
329
|
+
}
|
|
330
|
+
function labelOperation(record) {
|
|
331
|
+
const operationId = stringValue(record.operation.operationId);
|
|
332
|
+
return `${record.file}: ${record.method} ${record.path}${operationId ? ` (${operationId})` : ""}`;
|
|
333
|
+
}
|
|
334
|
+
function weakText(value, minLength) {
|
|
335
|
+
return !value || value.trim().length < minLength;
|
|
336
|
+
}
|
|
337
|
+
function asObject(value) {
|
|
338
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
339
|
+
? value
|
|
340
|
+
: undefined;
|
|
341
|
+
}
|
|
342
|
+
function stringValue(value) {
|
|
343
|
+
return typeof value === "string" ? value : undefined;
|
|
344
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Analyzer, Finding, ProjectSignals, ScanProfile } from "../schemas/types.js";
|
|
2
|
+
export declare class ReadinessAnalyzer implements Analyzer {
|
|
3
|
+
private readonly profile;
|
|
4
|
+
readonly id = "readiness-analyzer";
|
|
5
|
+
constructor(profile?: ScanProfile);
|
|
6
|
+
analyze(signals: ProjectSignals): Finding[];
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=readiness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../../src/analyzers/readiness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAI1F,qBAAa,iBAAkB,YAAW,QAAQ;IAGpC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,QAAQ,CAAC,EAAE,wBAAwB;gBAEN,OAAO,GAAE,WAAuB;IAE7D,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,EAAE;CAa5C"}
|