@farming-labs/docs 0.2.62 → 0.2.64
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/dist/agent-CQTH7NFu.mjs +624 -0
- package/dist/agent-DKKptIgy.mjs +4365 -0
- package/dist/agent-evals-B7MIxuEX.mjs +2144 -0
- package/dist/agent-export-CBgWgPvH.mjs +910 -0
- package/dist/agent-scope-C_U--OZ7.mjs +283 -0
- package/dist/agent-skills-bundle.d.mts +13 -0
- package/dist/agent-skills-bundle.mjs +12 -0
- package/dist/agent-skills-server-CPja6Syt.d.mts +14 -0
- package/dist/agent-skills-server-DraIb6FV.mjs +415 -0
- package/dist/agent-skills-vite.d.mts +31 -0
- package/dist/agent-skills-vite.mjs +70 -0
- package/dist/agents-XWZBub6f.mjs +221 -0
- package/dist/analytics-Bx44lg6d.mjs +177 -0
- package/dist/cli/index.d.mts +15 -0
- package/dist/cli/index.mjs +452 -0
- package/dist/client/react.d.mts +45 -0
- package/dist/client/react.mjs +223 -0
- package/dist/cloud-analytics-CSyFE6SS.mjs +132 -0
- package/dist/cloud-ask-ai-sbpjOR2K.mjs +382 -0
- package/dist/cloud-ask-ai-zpwkdwnF.d.mts +23 -0
- package/dist/cloud-pdNC-tyj.mjs +1615 -0
- package/dist/code-blocks-DnNVNK2M.mjs +871 -0
- package/dist/codeblocks-CFuurVIH.mjs +250 -0
- package/dist/config-Wcdj-D0a.mjs +369 -0
- package/dist/dev-Cmy6DtdF.mjs +1333 -0
- package/dist/docs-cloud-server.d.mts +70 -0
- package/dist/docs-cloud-server.mjs +310 -0
- package/dist/doctor-DtGYZ41i.mjs +2036 -0
- package/dist/downgrade-w7e6Se0L.mjs +184 -0
- package/dist/errors-DbOhkE1h.mjs +20 -0
- package/dist/golden-evaluations-Dj-9Eo3v.mjs +1785 -0
- package/dist/i18n-CCaFUnAN.mjs +40 -0
- package/dist/index.d.mts +1150 -0
- package/dist/index.mjs +10 -0
- package/dist/init-CQY0Woe3.mjs +1264 -0
- package/dist/mcp-B9dcsivk.mjs +156 -0
- package/dist/mcp.d.mts +298 -0
- package/dist/mcp.mjs +4430 -0
- package/dist/metadata-DWExHQnx.mjs +237 -0
- package/dist/package-version-n5AFur8a.mjs +128 -0
- package/dist/reading-time-CYZ5VvKU.mjs +742 -0
- package/dist/review-CLoHTywU.mjs +673 -0
- package/dist/robots-BIpC4j4P.mjs +201 -0
- package/dist/robots-CUTahhoY.mjs +179 -0
- package/dist/search-B6V6qtiI.mjs +1826 -0
- package/dist/search-CaSyi6H6.d.mts +279 -0
- package/dist/search-DSjCeOk7.mjs +104 -0
- package/dist/server.d.mts +343 -0
- package/dist/server.mjs +14 -0
- package/dist/sitemap-Cykpe3Tz.mjs +249 -0
- package/dist/sitemap-server-C_6Wes83.mjs +1137 -0
- package/dist/standards-discovery-C4HUqMd2.d.mts +227 -0
- package/dist/standards-discovery-jkykaXq1.mjs +519 -0
- package/dist/templates-Bq_P7ctv.mjs +2465 -0
- package/dist/types-lMBIdZg0.d.mts +3315 -0
- package/dist/upgrade-oz-GChgt.mjs +56 -0
- package/dist/utils-DpiIioYb.mjs +225 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
import { Jt as normalizePageAgentFrontmatter, qt as hasStructuredPageAgentContract, wt as resolveDocsAudienceMdxContent, xt as findDocsAudienceMdxTags } from "./agent-DKKptIgy.mjs";
|
|
2
|
+
import { n as agentVersionConstraintsOverlap, o as normalizeAgentVersion, r as normalizeAgentFramework } from "./agent-scope-C_U--OZ7.mjs";
|
|
3
|
+
import { t as extractCodeBlocksFromMarkdown } from "./code-blocks-DnNVNK2M.mjs";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/agent-usefulness.ts
|
|
8
|
+
/** Build analyzer inputs from the same filesystem MCP pages used by doctor and review. */
|
|
9
|
+
function createAgentUsefulnessPagesFromMcp(rootDir, pages) {
|
|
10
|
+
return pages.map((page) => {
|
|
11
|
+
const sourcePath = page.sourcePath ?? page.url;
|
|
12
|
+
const absoluteSourcePath = page.sourcePath ? path.isAbsolute(page.sourcePath) ? page.sourcePath : path.join(rootDir, page.sourcePath) : void 0;
|
|
13
|
+
const source = absoluteSourcePath && existsSync(absoluteSourcePath) ? readFileSync(absoluteSourcePath, "utf-8") : page.rawContent ?? page.agentFallbackRawContent ?? page.content;
|
|
14
|
+
const absoluteAgentPath = Boolean(absoluteSourcePath && /(?:^|\/)(?:page|index|\+page)\.(?:md|mdx|svx)$/i.test(absoluteSourcePath)) && absoluteSourcePath ? path.join(path.dirname(absoluteSourcePath), "agent.md") : void 0;
|
|
15
|
+
const hasAgentSource = Boolean(absoluteAgentPath && existsSync(absoluteAgentPath));
|
|
16
|
+
return {
|
|
17
|
+
route: page.url,
|
|
18
|
+
sourcePath,
|
|
19
|
+
source,
|
|
20
|
+
agentSourcePath: hasAgentSource && absoluteAgentPath ? path.relative(rootDir, absoluteAgentPath).replace(/\\/g, "/") : void 0,
|
|
21
|
+
agentSource: hasAgentSource && absoluteAgentPath ? readFileSync(absoluteAgentPath, "utf-8") : void 0,
|
|
22
|
+
title: page.title,
|
|
23
|
+
description: page.description,
|
|
24
|
+
related: page.related,
|
|
25
|
+
framework: page.framework,
|
|
26
|
+
version: page.version,
|
|
27
|
+
agent: page.agent
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const DEFAULT_DOCS_COMMANDS = [
|
|
32
|
+
"init",
|
|
33
|
+
"dev",
|
|
34
|
+
"deploy",
|
|
35
|
+
"preview",
|
|
36
|
+
"cloud init",
|
|
37
|
+
"cloud sync",
|
|
38
|
+
"cloud check",
|
|
39
|
+
"cloud deploy",
|
|
40
|
+
"cloud preview",
|
|
41
|
+
"mcp",
|
|
42
|
+
"agent compact",
|
|
43
|
+
"agent export",
|
|
44
|
+
"agents generate",
|
|
45
|
+
"doctor",
|
|
46
|
+
"review",
|
|
47
|
+
"codeblocks validate",
|
|
48
|
+
"code-blocks validate",
|
|
49
|
+
"search sync",
|
|
50
|
+
"sitemap generate",
|
|
51
|
+
"robots generate",
|
|
52
|
+
"upgrade",
|
|
53
|
+
"downgrade"
|
|
54
|
+
];
|
|
55
|
+
const ACTIONABLE_PATTERN = /\b(?:add|authenticate|build|configure|create|debug|deploy|disable|enable|fetch|fix|implement|install|integrate|migrate|open|remove|run|set|set\s*up|test|troubleshoot|update|upgrade)\b/i;
|
|
56
|
+
const PREREQUISITE_PATTERN = /(?:^|\n)\s{0,3}#{1,6}\s+(?:before you begin|prerequisites?|requirements?|setup)\b|\b(?:before (?:starting|you begin)|requires? (?:an?|the|you))\b/i;
|
|
57
|
+
const EXPECTED_RESULT_PATTERN = /(?:^|\n)\s{0,3}#{1,6}\s+(?:expected (?:result|output)|outcome|result|success|verification)\b|\b(?:you should see|expected to (?:see|return|produce)|succeeds? when|tests? pass(?:es)?)\b/i;
|
|
58
|
+
const RECOVERY_PATTERN = /(?:^|\n)\s{0,3}#{1,6}\s+(?:failure modes?|recovery|rollback|troubleshooting)\b|\b(?:if .{0,80} fails?|recover(?:y)?|restore|retry|revert|roll back|undo)\b/i;
|
|
59
|
+
const STRONG_SPECIFIC_CONTEXT_PATTERN = /(?:@[a-z0-9_-]+\/[a-z0-9_.-]+|(?:^|\s)--[a-z][\w-]*|(?:^|\s)(?:\.\.?\/|\/)[\w./-]+|\b[\w-]+\.(?:[cm]?[jt]sx?|json|mdx?|vue|svelte|astro)\b|\bv?\d+(?:\.\d+)+\b)/im;
|
|
60
|
+
const GENERIC_SENTENCE_PATTERN = /\b(?:follow|keep answers|refer to|read|use) (?:the |this )?(?:documentation|docs|instructions|page)\b|\bkeep answers grounded\b|\bpoint to (?:the )?(?:closest )?related docs\b|\bif the request moves beyond this page\b|\bdo not (?:guess|invent)\b/i;
|
|
61
|
+
const FRAMEWORK_PATTERNS = [
|
|
62
|
+
["nextjs", /\bNext\.js\b|@farming-labs\/next\b|\bnext\.config\b|\bApp Router\b/i],
|
|
63
|
+
["tanstackstart", /\bTanStack Start\b|@farming-labs\/(?:tanstack|tanstack-start)\b|\bcreateFileRoute\b/i],
|
|
64
|
+
["sveltekit", /\bSvelteKit\b|@farming-labs\/svelte(?:kit)?\b|\bhooks\.server\b|\+server\.[cm]?[jt]s\b/i],
|
|
65
|
+
["astro", /\bAstro\b|@farming-labs\/astro\b|\bastro\.config\b/i],
|
|
66
|
+
["nuxt", /\bNuxt\b|@farming-labs\/nuxt\b|\bnuxt\.config\b/i]
|
|
67
|
+
];
|
|
68
|
+
const PACKAGE_MANAGER_BUILT_INS = {
|
|
69
|
+
npm: new Set([
|
|
70
|
+
"access",
|
|
71
|
+
"adduser",
|
|
72
|
+
"audit",
|
|
73
|
+
"cache",
|
|
74
|
+
"ci",
|
|
75
|
+
"config",
|
|
76
|
+
"dedupe",
|
|
77
|
+
"exec",
|
|
78
|
+
"help",
|
|
79
|
+
"init",
|
|
80
|
+
"install",
|
|
81
|
+
"link",
|
|
82
|
+
"login",
|
|
83
|
+
"logout",
|
|
84
|
+
"org",
|
|
85
|
+
"outdated",
|
|
86
|
+
"owner",
|
|
87
|
+
"pack",
|
|
88
|
+
"ping",
|
|
89
|
+
"prefix",
|
|
90
|
+
"profile",
|
|
91
|
+
"prune",
|
|
92
|
+
"publish",
|
|
93
|
+
"query",
|
|
94
|
+
"rebuild",
|
|
95
|
+
"repo",
|
|
96
|
+
"restart",
|
|
97
|
+
"root",
|
|
98
|
+
"run",
|
|
99
|
+
"run-script",
|
|
100
|
+
"search",
|
|
101
|
+
"shrinkwrap",
|
|
102
|
+
"star",
|
|
103
|
+
"stars",
|
|
104
|
+
"start",
|
|
105
|
+
"stop",
|
|
106
|
+
"team",
|
|
107
|
+
"test",
|
|
108
|
+
"token",
|
|
109
|
+
"uninstall",
|
|
110
|
+
"unpublish",
|
|
111
|
+
"unstar",
|
|
112
|
+
"update",
|
|
113
|
+
"version",
|
|
114
|
+
"view",
|
|
115
|
+
"whoami"
|
|
116
|
+
]),
|
|
117
|
+
pnpm: new Set([
|
|
118
|
+
"add",
|
|
119
|
+
"approve-builds",
|
|
120
|
+
"audit",
|
|
121
|
+
"bin",
|
|
122
|
+
"config",
|
|
123
|
+
"create",
|
|
124
|
+
"dedupe",
|
|
125
|
+
"deploy",
|
|
126
|
+
"dlx",
|
|
127
|
+
"env",
|
|
128
|
+
"exec",
|
|
129
|
+
"fetch",
|
|
130
|
+
"help",
|
|
131
|
+
"import",
|
|
132
|
+
"init",
|
|
133
|
+
"install",
|
|
134
|
+
"link",
|
|
135
|
+
"list",
|
|
136
|
+
"outdated",
|
|
137
|
+
"pack",
|
|
138
|
+
"patch",
|
|
139
|
+
"patch-commit",
|
|
140
|
+
"prune",
|
|
141
|
+
"publish",
|
|
142
|
+
"rebuild",
|
|
143
|
+
"remove",
|
|
144
|
+
"root",
|
|
145
|
+
"run",
|
|
146
|
+
"server",
|
|
147
|
+
"setup",
|
|
148
|
+
"store",
|
|
149
|
+
"test",
|
|
150
|
+
"unlink",
|
|
151
|
+
"update",
|
|
152
|
+
"view",
|
|
153
|
+
"why"
|
|
154
|
+
]),
|
|
155
|
+
yarn: new Set([
|
|
156
|
+
"add",
|
|
157
|
+
"bin",
|
|
158
|
+
"cache",
|
|
159
|
+
"config",
|
|
160
|
+
"create",
|
|
161
|
+
"dedupe",
|
|
162
|
+
"dlx",
|
|
163
|
+
"exec",
|
|
164
|
+
"help",
|
|
165
|
+
"info",
|
|
166
|
+
"init",
|
|
167
|
+
"install",
|
|
168
|
+
"link",
|
|
169
|
+
"npm",
|
|
170
|
+
"pack",
|
|
171
|
+
"plugin",
|
|
172
|
+
"rebuild",
|
|
173
|
+
"remove",
|
|
174
|
+
"run",
|
|
175
|
+
"set",
|
|
176
|
+
"stage",
|
|
177
|
+
"unlink",
|
|
178
|
+
"up",
|
|
179
|
+
"why",
|
|
180
|
+
"workspace",
|
|
181
|
+
"workspaces"
|
|
182
|
+
]),
|
|
183
|
+
bun: new Set([
|
|
184
|
+
"add",
|
|
185
|
+
"build",
|
|
186
|
+
"create",
|
|
187
|
+
"dev",
|
|
188
|
+
"help",
|
|
189
|
+
"install",
|
|
190
|
+
"link",
|
|
191
|
+
"pm",
|
|
192
|
+
"publish",
|
|
193
|
+
"remove",
|
|
194
|
+
"repl",
|
|
195
|
+
"run",
|
|
196
|
+
"test",
|
|
197
|
+
"unlink",
|
|
198
|
+
"update",
|
|
199
|
+
"upgrade",
|
|
200
|
+
"x"
|
|
201
|
+
])
|
|
202
|
+
};
|
|
203
|
+
const PACKAGE_MANAGER_OPTIONS_WITH_VALUES = new Set([
|
|
204
|
+
"--cwd",
|
|
205
|
+
"--dir",
|
|
206
|
+
"--filter",
|
|
207
|
+
"--prefix",
|
|
208
|
+
"--workspace",
|
|
209
|
+
"-C",
|
|
210
|
+
"-F",
|
|
211
|
+
"-w"
|
|
212
|
+
]);
|
|
213
|
+
const CURL_OPTIONS_WITH_VALUES = new Set([
|
|
214
|
+
"--cacert",
|
|
215
|
+
"--cert",
|
|
216
|
+
"--connect-timeout",
|
|
217
|
+
"--data",
|
|
218
|
+
"--data-ascii",
|
|
219
|
+
"--data-binary",
|
|
220
|
+
"--data-raw",
|
|
221
|
+
"--data-urlencode",
|
|
222
|
+
"--form",
|
|
223
|
+
"--header",
|
|
224
|
+
"--json",
|
|
225
|
+
"--key",
|
|
226
|
+
"--max-time",
|
|
227
|
+
"--output",
|
|
228
|
+
"--request",
|
|
229
|
+
"--resolve",
|
|
230
|
+
"--retry",
|
|
231
|
+
"--retry-delay",
|
|
232
|
+
"--url",
|
|
233
|
+
"--user",
|
|
234
|
+
"--user-agent"
|
|
235
|
+
]);
|
|
236
|
+
const CURL_OPTIONS_WITHOUT_VALUES = new Set([
|
|
237
|
+
"--compressed",
|
|
238
|
+
"--fail",
|
|
239
|
+
"--fail-with-body",
|
|
240
|
+
"--head",
|
|
241
|
+
"--include",
|
|
242
|
+
"--insecure",
|
|
243
|
+
"--location",
|
|
244
|
+
"--show-error",
|
|
245
|
+
"--silent",
|
|
246
|
+
"--verbose"
|
|
247
|
+
]);
|
|
248
|
+
const CURL_SHORT_OPTIONS_WITH_VALUES = new Set([
|
|
249
|
+
"A",
|
|
250
|
+
"d",
|
|
251
|
+
"F",
|
|
252
|
+
"H",
|
|
253
|
+
"o",
|
|
254
|
+
"u",
|
|
255
|
+
"X"
|
|
256
|
+
]);
|
|
257
|
+
const CURL_SHORT_OPTIONS_WITHOUT_VALUES = new Set([
|
|
258
|
+
"f",
|
|
259
|
+
"I",
|
|
260
|
+
"i",
|
|
261
|
+
"k",
|
|
262
|
+
"L",
|
|
263
|
+
"s",
|
|
264
|
+
"S",
|
|
265
|
+
"v"
|
|
266
|
+
]);
|
|
267
|
+
const TEST_UNARY_OPERATORS = new Set([
|
|
268
|
+
"-b",
|
|
269
|
+
"-c",
|
|
270
|
+
"-d",
|
|
271
|
+
"-e",
|
|
272
|
+
"-f",
|
|
273
|
+
"-g",
|
|
274
|
+
"-h",
|
|
275
|
+
"-L",
|
|
276
|
+
"-n",
|
|
277
|
+
"-p",
|
|
278
|
+
"-r",
|
|
279
|
+
"-S",
|
|
280
|
+
"-s",
|
|
281
|
+
"-t",
|
|
282
|
+
"-u",
|
|
283
|
+
"-w",
|
|
284
|
+
"-x",
|
|
285
|
+
"-z"
|
|
286
|
+
]);
|
|
287
|
+
const TEST_BINARY_OPERATORS = new Set([
|
|
288
|
+
"=",
|
|
289
|
+
"==",
|
|
290
|
+
"!=",
|
|
291
|
+
"-ef",
|
|
292
|
+
"-eq",
|
|
293
|
+
"-ge",
|
|
294
|
+
"-gt",
|
|
295
|
+
"-le",
|
|
296
|
+
"-lt",
|
|
297
|
+
"-ne",
|
|
298
|
+
"-nt",
|
|
299
|
+
"-ot"
|
|
300
|
+
]);
|
|
301
|
+
function isAgentContextVisible(scopes) {
|
|
302
|
+
return scopes.every((scope) => scope.only === void 0 || scope.only === "agent");
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Extract actual agent-only context while ignoring examples in fenced and inline code.
|
|
306
|
+
*
|
|
307
|
+
* `<Agent>` is agent-only shorthand. `<Audience>` counts only when `only` is the
|
|
308
|
+
* static value `"agent"`; human-only and dynamic/invalid audience declarations do
|
|
309
|
+
* not contribute to agent-context usefulness metrics.
|
|
310
|
+
*/
|
|
311
|
+
function extractAgentBlocks(source, options = {}) {
|
|
312
|
+
const sourcePath = options.sourcePath ?? "unknown";
|
|
313
|
+
const tags = findDocsAudienceMdxTags(source);
|
|
314
|
+
const blocks = [];
|
|
315
|
+
const scopes = [];
|
|
316
|
+
let nextScopeId = 1;
|
|
317
|
+
let cursor = 0;
|
|
318
|
+
let active;
|
|
319
|
+
const appendVisibleContent = (content) => {
|
|
320
|
+
if (active && isAgentContextVisible(scopes)) active.content.push(content);
|
|
321
|
+
};
|
|
322
|
+
const finishActiveBlock = () => {
|
|
323
|
+
if (!active) return;
|
|
324
|
+
blocks.push({
|
|
325
|
+
sourcePath,
|
|
326
|
+
route: options.route,
|
|
327
|
+
line: active.line,
|
|
328
|
+
content: active.content.join("").trim()
|
|
329
|
+
});
|
|
330
|
+
active = void 0;
|
|
331
|
+
};
|
|
332
|
+
for (const tag of tags) {
|
|
333
|
+
const activeScope = scopes.at(-1);
|
|
334
|
+
if (tag.closing && activeScope?.name !== tag.name) continue;
|
|
335
|
+
appendVisibleContent(source.slice(cursor, tag.index));
|
|
336
|
+
cursor = tag.end;
|
|
337
|
+
if (tag.closing) {
|
|
338
|
+
const closedScope = scopes.pop();
|
|
339
|
+
if (active && closedScope?.id === active.scopeId) finishActiveBlock();
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (tag.selfClosing) continue;
|
|
343
|
+
const scope = {
|
|
344
|
+
id: nextScopeId,
|
|
345
|
+
name: tag.name,
|
|
346
|
+
only: tag.only
|
|
347
|
+
};
|
|
348
|
+
nextScopeId += 1;
|
|
349
|
+
scopes.push(scope);
|
|
350
|
+
const isAgentOnly = tag.name === "Agent" || tag.name === "Audience" && tag.only === "agent";
|
|
351
|
+
if (!active && isAgentOnly && isAgentContextVisible(scopes)) active = {
|
|
352
|
+
line: source.slice(0, tag.index).split(/\r?\n/).length,
|
|
353
|
+
scopeId: scope.id,
|
|
354
|
+
content: []
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
appendVisibleContent(source.slice(cursor));
|
|
358
|
+
return blocks;
|
|
359
|
+
}
|
|
360
|
+
/** Analyze page-level context quality without executing documented commands. */
|
|
361
|
+
function analyzeAgentUsefulness(options) {
|
|
362
|
+
const rootDir = path.resolve(options.rootDir);
|
|
363
|
+
const packageManager = options.packageManager ?? detectPackageManager(rootDir);
|
|
364
|
+
const knownDocsCommands = new Set([...DEFAULT_DOCS_COMMANDS, ...options.knownDocsCommands ?? []]);
|
|
365
|
+
const workspaceRoot = findEnclosingWorkspaceRoot(rootDir);
|
|
366
|
+
const packageManifests = readProjectPackageManifests(workspaceRoot);
|
|
367
|
+
const pages = options.pages.map((page) => analyzePage(page));
|
|
368
|
+
const findings = [];
|
|
369
|
+
const unhealthyCommandKeys = /* @__PURE__ */ new Set();
|
|
370
|
+
const unverifiedCommandKeys = /* @__PURE__ */ new Set();
|
|
371
|
+
const blockQuality = analyzeBlockQuality(pages, options.boilerplate);
|
|
372
|
+
findings.push(...blockQuality.findings);
|
|
373
|
+
let completeTaskPages = 0;
|
|
374
|
+
let missingPrerequisites = 0;
|
|
375
|
+
let missingExpectedResults = 0;
|
|
376
|
+
let missingRecovery = 0;
|
|
377
|
+
let conflictingPages = 0;
|
|
378
|
+
let ambiguousPages = 0;
|
|
379
|
+
let mismatchedPages = 0;
|
|
380
|
+
let commandCount = 0;
|
|
381
|
+
let coveredActionablePages = 0;
|
|
382
|
+
let missingRelatedPages = 0;
|
|
383
|
+
let brokenRelatedLinks = 0;
|
|
384
|
+
const knownRoutes = new Set(options.pages.map((page) => normalizeRoute(page.route)));
|
|
385
|
+
for (const analysis of pages) {
|
|
386
|
+
const { page, agent, actionable, projectedSource } = analysis;
|
|
387
|
+
if (actionable) {
|
|
388
|
+
const guidance = [
|
|
389
|
+
stripFencedContent(projectedSource),
|
|
390
|
+
page.agentSource ? stripFencedContent(page.agentSource) : void 0,
|
|
391
|
+
...analysis.blocks.map((block) => stripFencedContent(block.content))
|
|
392
|
+
].filter((value) => Boolean(value)).join("\n\n");
|
|
393
|
+
const taskIssuesBefore = findings.length;
|
|
394
|
+
if (!agent?.prerequisites?.length && !PREREQUISITE_PATTERN.test(guidance)) {
|
|
395
|
+
missingPrerequisites += 1;
|
|
396
|
+
findings.push(makeFinding(page, {
|
|
397
|
+
code: "task-missing-prerequisites",
|
|
398
|
+
severity: "warning",
|
|
399
|
+
category: "task",
|
|
400
|
+
message: "Actionable agent guidance is missing prerequisites or an explicit before-you-begin condition."
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
if (!hasExpectedResult(agent) && !EXPECTED_RESULT_PATTERN.test(guidance)) {
|
|
404
|
+
missingExpectedResults += 1;
|
|
405
|
+
findings.push(makeFinding(page, {
|
|
406
|
+
code: "task-missing-expected-result",
|
|
407
|
+
severity: "warning",
|
|
408
|
+
category: "task",
|
|
409
|
+
message: "Actionable agent guidance is missing an observable outcome or expected verification result."
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
if (!hasRecovery(agent) && !RECOVERY_PATTERN.test(guidance)) {
|
|
413
|
+
missingRecovery += 1;
|
|
414
|
+
findings.push(makeFinding(page, {
|
|
415
|
+
code: "task-missing-recovery",
|
|
416
|
+
severity: "warning",
|
|
417
|
+
category: "task",
|
|
418
|
+
message: "Actionable agent guidance is missing rollback, recovery, or resolved failure-mode steps."
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
if (findings.length === taskIssuesBefore) completeTaskPages += 1;
|
|
422
|
+
}
|
|
423
|
+
const applicability = analyzeApplicability(page, agent, actionable, options.projectFramework);
|
|
424
|
+
findings.push(...applicability.findings);
|
|
425
|
+
if (applicability.conflicting) conflictingPages += 1;
|
|
426
|
+
if (applicability.ambiguous) ambiguousPages += 1;
|
|
427
|
+
if (applicability.mismatched) mismatchedPages += 1;
|
|
428
|
+
for (const [commandIndex, command] of analysis.shellCommands.entries()) {
|
|
429
|
+
commandCount += 1;
|
|
430
|
+
const commandKey = `${page.sourcePath}:${command.line ?? 1}:${commandIndex}`;
|
|
431
|
+
const commandAnalysis = analyzeCommand({
|
|
432
|
+
page,
|
|
433
|
+
command,
|
|
434
|
+
rootDir,
|
|
435
|
+
packageManager,
|
|
436
|
+
knownDocsCommands,
|
|
437
|
+
packageManifests,
|
|
438
|
+
workspaceRoot
|
|
439
|
+
});
|
|
440
|
+
if (commandAnalysis.status === "unhealthy") unhealthyCommandKeys.add(commandKey);
|
|
441
|
+
if (commandAnalysis.status === "unverified") unverifiedCommandKeys.add(commandKey);
|
|
442
|
+
findings.push(...commandAnalysis.findings);
|
|
443
|
+
}
|
|
444
|
+
if (actionable) {
|
|
445
|
+
const related = normalizeRelated(page.related);
|
|
446
|
+
const validRelated = related.filter((href) => {
|
|
447
|
+
const route = internalRelatedRoute(href);
|
|
448
|
+
return route !== void 0 && knownRoutes.has(route);
|
|
449
|
+
});
|
|
450
|
+
for (const href of related) {
|
|
451
|
+
const route = internalRelatedRoute(href);
|
|
452
|
+
if (!route || knownRoutes.has(route)) continue;
|
|
453
|
+
brokenRelatedLinks += 1;
|
|
454
|
+
findings.push(makeFinding(page, {
|
|
455
|
+
code: "related-broken",
|
|
456
|
+
severity: "warning",
|
|
457
|
+
category: "related",
|
|
458
|
+
message: `Related page does not resolve to a known docs route: ${href}`
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
if (validRelated.length > 0) coveredActionablePages += 1;
|
|
462
|
+
else {
|
|
463
|
+
missingRelatedPages += 1;
|
|
464
|
+
findings.push(makeFinding(page, {
|
|
465
|
+
code: "related-missing",
|
|
466
|
+
severity: "suggestion",
|
|
467
|
+
category: "related",
|
|
468
|
+
message: "Actionable agent guidance has no valid related-page route."
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const actionablePages = pages.filter((page) => page.actionable).length;
|
|
474
|
+
const usefulBlocks = Math.max(0, blockQuality.totalBlocks - blockQuality.lowQualityBlockKeys.size);
|
|
475
|
+
return {
|
|
476
|
+
findings: findings.sort(compareFindings),
|
|
477
|
+
metrics: {
|
|
478
|
+
totalPages: pages.length,
|
|
479
|
+
actionablePages,
|
|
480
|
+
agentBlocks: {
|
|
481
|
+
total: blockQuality.totalBlocks,
|
|
482
|
+
pages: pages.filter((page) => page.blocks.length > 0).length,
|
|
483
|
+
duplicate: blockQuality.duplicateBlockKeys.size,
|
|
484
|
+
boilerplate: blockQuality.boilerplateBlockKeys.size,
|
|
485
|
+
generic: blockQuality.genericBlockKeys.size,
|
|
486
|
+
useful: usefulBlocks
|
|
487
|
+
},
|
|
488
|
+
taskCompleteness: {
|
|
489
|
+
completePages: completeTaskPages,
|
|
490
|
+
missingPrerequisites,
|
|
491
|
+
missingExpectedResults,
|
|
492
|
+
missingRecovery,
|
|
493
|
+
coverage: percentage(completeTaskPages, actionablePages)
|
|
494
|
+
},
|
|
495
|
+
applicability: {
|
|
496
|
+
conflictingPages,
|
|
497
|
+
ambiguousPages,
|
|
498
|
+
mismatchedPages
|
|
499
|
+
},
|
|
500
|
+
commands: {
|
|
501
|
+
total: commandCount,
|
|
502
|
+
healthy: Math.max(0, commandCount - unhealthyCommandKeys.size - unverifiedCommandKeys.size),
|
|
503
|
+
unhealthy: unhealthyCommandKeys.size,
|
|
504
|
+
unverified: unverifiedCommandKeys.size
|
|
505
|
+
},
|
|
506
|
+
related: {
|
|
507
|
+
coveredActionablePages,
|
|
508
|
+
missingActionablePages: missingRelatedPages,
|
|
509
|
+
brokenLinks: brokenRelatedLinks,
|
|
510
|
+
coverage: percentage(coveredActionablePages, actionablePages)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function analyzePage(page) {
|
|
516
|
+
const agent = normalizePageAgentFrontmatter(page.agent);
|
|
517
|
+
const projectedSource = resolveDocsAudienceMdxContent(page.source, "agent");
|
|
518
|
+
const blocks = extractAgentBlocks(page.source, {
|
|
519
|
+
sourcePath: page.sourcePath,
|
|
520
|
+
route: page.route
|
|
521
|
+
});
|
|
522
|
+
const shellCommands = collectPageCommands(page, agent, projectedSource);
|
|
523
|
+
return {
|
|
524
|
+
page,
|
|
525
|
+
agent,
|
|
526
|
+
blocks,
|
|
527
|
+
projectedSource,
|
|
528
|
+
shellCommands,
|
|
529
|
+
actionable: page.actionable ?? (hasStructuredPageAgentContract(agent) || shellCommands.length > 0 || blocks.some((block) => ACTIONABLE_PATTERN.test(block.content)) || ACTIONABLE_PATTERN.test(page.agentSource ?? ""))
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
function analyzeBlockQuality(pages, config) {
|
|
533
|
+
const findings = [];
|
|
534
|
+
const allBlocks = pages.flatMap((analysis) => analysis.blocks.map((block, index) => ({
|
|
535
|
+
analysis,
|
|
536
|
+
block,
|
|
537
|
+
index
|
|
538
|
+
})));
|
|
539
|
+
const signatureGroups = /* @__PURE__ */ new Map();
|
|
540
|
+
const sentenceOccurrences = /* @__PURE__ */ new Map();
|
|
541
|
+
const blockSentences = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const item of allBlocks) {
|
|
543
|
+
const key = blockKey(item.block, item.index);
|
|
544
|
+
const sentences = splitNormalizedSentences(item.block.content);
|
|
545
|
+
blockSentences.set(key, sentences);
|
|
546
|
+
const signature = normalizeBlockSignature(item.block.content);
|
|
547
|
+
if (signature) {
|
|
548
|
+
const group = signatureGroups.get(signature) ?? [];
|
|
549
|
+
group.push(item);
|
|
550
|
+
signatureGroups.set(signature, group);
|
|
551
|
+
}
|
|
552
|
+
for (const sentence of new Set(sentences)) {
|
|
553
|
+
const occurrences = sentenceOccurrences.get(sentence) ?? /* @__PURE__ */ new Set();
|
|
554
|
+
occurrences.add(key);
|
|
555
|
+
sentenceOccurrences.set(sentence, occurrences);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const minOccurrences = Math.max(2, Math.round(config?.minOccurrences ?? 3));
|
|
559
|
+
const corpusRatio = clampRatio(config?.corpusRatio ?? .2);
|
|
560
|
+
const repeatedThreshold = Math.min(minOccurrences, Math.max(2, Math.ceil(allBlocks.length * corpusRatio)));
|
|
561
|
+
const blockRatio = clampRatio(config?.blockRatio ?? .5);
|
|
562
|
+
const repeatedSentences = new Set([...sentenceOccurrences].filter(([, occurrences]) => occurrences.size >= repeatedThreshold).map(([sentence]) => sentence));
|
|
563
|
+
const duplicateBlockKeys = /* @__PURE__ */ new Set();
|
|
564
|
+
const boilerplateBlockKeys = /* @__PURE__ */ new Set();
|
|
565
|
+
const genericBlockKeys = /* @__PURE__ */ new Set();
|
|
566
|
+
const lowQualityBlockKeys = /* @__PURE__ */ new Set();
|
|
567
|
+
for (const group of signatureGroups.values()) {
|
|
568
|
+
if (group.length < 2) continue;
|
|
569
|
+
const relatedFiles = Array.from(new Set(group.map((item) => item.block.sourcePath))).sort();
|
|
570
|
+
for (const item of group) {
|
|
571
|
+
const key = blockKey(item.block, item.index);
|
|
572
|
+
duplicateBlockKeys.add(key);
|
|
573
|
+
lowQualityBlockKeys.add(key);
|
|
574
|
+
findings.push(makeFinding(item.analysis.page, {
|
|
575
|
+
code: "agent-block-duplicate",
|
|
576
|
+
severity: "warning",
|
|
577
|
+
category: "context",
|
|
578
|
+
line: item.block.line,
|
|
579
|
+
relatedFiles: relatedFiles.filter((file) => file !== item.block.sourcePath),
|
|
580
|
+
message: `Agent-only block duplicates context used on ${group.length - 1} other page${group.length === 2 ? "" : "s"}.`
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
for (const item of allBlocks) {
|
|
585
|
+
const key = blockKey(item.block, item.index);
|
|
586
|
+
const sentences = blockSentences.get(key) ?? [];
|
|
587
|
+
const totalChars = sentences.reduce((total, sentence) => total + sentence.length, 0);
|
|
588
|
+
const repeatedChars = sentences.filter((sentence) => repeatedSentences.has(sentence)).reduce((total, sentence) => total + sentence.length, 0);
|
|
589
|
+
const ratio = totalChars > 0 ? repeatedChars / totalChars : 0;
|
|
590
|
+
if (ratio >= blockRatio) {
|
|
591
|
+
boilerplateBlockKeys.add(key);
|
|
592
|
+
lowQualityBlockKeys.add(key);
|
|
593
|
+
if (!duplicateBlockKeys.has(key)) findings.push(makeFinding(item.analysis.page, {
|
|
594
|
+
code: "agent-block-boilerplate",
|
|
595
|
+
severity: "warning",
|
|
596
|
+
category: "context",
|
|
597
|
+
line: item.block.line,
|
|
598
|
+
message: `${Math.round(ratio * 100)}% of this agent-only block repeats sentences used across the docs corpus.`
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
if (isGenericAgentBlock(item.block.content, sentences)) {
|
|
602
|
+
genericBlockKeys.add(key);
|
|
603
|
+
lowQualityBlockKeys.add(key);
|
|
604
|
+
findings.push(makeFinding(item.analysis.page, {
|
|
605
|
+
code: "agent-block-generic",
|
|
606
|
+
severity: "suggestion",
|
|
607
|
+
category: "context",
|
|
608
|
+
line: item.block.line,
|
|
609
|
+
message: "Agent-only block is generic and lacks a concrete command, path, identifier, version, or task-specific constraint."
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
findings,
|
|
615
|
+
totalBlocks: allBlocks.length,
|
|
616
|
+
duplicateBlockKeys,
|
|
617
|
+
boilerplateBlockKeys,
|
|
618
|
+
genericBlockKeys,
|
|
619
|
+
lowQualityBlockKeys
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function analyzeApplicability(page, agent, actionable, projectFramework) {
|
|
623
|
+
const findings = [];
|
|
624
|
+
const topFrameworks = normalizeFrameworks(page.framework);
|
|
625
|
+
const contractFrameworks = normalizeFrameworks(agent?.appliesTo?.framework);
|
|
626
|
+
const topVersions = normalizeValues(page.version);
|
|
627
|
+
const contractVersions = normalizeValues(agent?.appliesTo?.version);
|
|
628
|
+
let conflicting = false;
|
|
629
|
+
let ambiguous = false;
|
|
630
|
+
let mismatched = false;
|
|
631
|
+
if (topFrameworks.length > 0 && contractFrameworks.length > 0 && !setsOverlap(topFrameworks, contractFrameworks)) {
|
|
632
|
+
conflicting = true;
|
|
633
|
+
findings.push(makeFinding(page, {
|
|
634
|
+
code: "applicability-framework-conflict",
|
|
635
|
+
severity: "warning",
|
|
636
|
+
category: "applicability",
|
|
637
|
+
message: `Top-level framework (${topFrameworks.join(", ")}) conflicts with agent.appliesTo.framework (${contractFrameworks.join(", ")}).`
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
if (topVersions.length > 0 && contractVersions.length > 0 && !versionSetsCompatible(topVersions, contractVersions)) {
|
|
641
|
+
conflicting = true;
|
|
642
|
+
findings.push(makeFinding(page, {
|
|
643
|
+
code: "applicability-version-conflict",
|
|
644
|
+
severity: "warning",
|
|
645
|
+
category: "applicability",
|
|
646
|
+
message: `Top-level version (${topVersions.join(", ")}) conflicts with agent.appliesTo.version (${contractVersions.join(", ")}).`
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
const declaredFrameworks = Array.from(new Set([...topFrameworks, ...contractFrameworks]));
|
|
650
|
+
const inferredFrameworks = inferFrameworks(`${page.source}\n${page.agentSource ?? ""}`);
|
|
651
|
+
if (actionable && declaredFrameworks.length === 0 && inferredFrameworks.length > 1) {
|
|
652
|
+
ambiguous = true;
|
|
653
|
+
findings.push(makeFinding(page, {
|
|
654
|
+
code: "applicability-framework-ambiguous",
|
|
655
|
+
severity: "warning",
|
|
656
|
+
category: "applicability",
|
|
657
|
+
message: `Actionable page references multiple frameworks (${inferredFrameworks.join(", ")}) without framework applicability metadata.`
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
660
|
+
const normalizedProjectFramework = projectFramework ? normalizeAgentFramework(projectFramework) : void 0;
|
|
661
|
+
const effectiveFrameworks = declaredFrameworks.length > 0 ? declaredFrameworks : inferredFrameworks;
|
|
662
|
+
if (actionable && normalizedProjectFramework && effectiveFrameworks.length > 0 && !effectiveFrameworks.includes(normalizedProjectFramework)) {
|
|
663
|
+
mismatched = true;
|
|
664
|
+
findings.push(makeFinding(page, {
|
|
665
|
+
code: "applicability-framework-mismatch",
|
|
666
|
+
severity: "warning",
|
|
667
|
+
category: "applicability",
|
|
668
|
+
message: `Page applicability (${effectiveFrameworks.join(", ")}) does not include the detected project framework (${normalizedProjectFramework}).`
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
if (actionable && topVersions.length === 0 && contractVersions.length === 0 && hasAmbiguousVersionSignals(`${page.source}\n${page.agentSource ?? ""}`)) {
|
|
672
|
+
ambiguous = true;
|
|
673
|
+
findings.push(makeFinding(page, {
|
|
674
|
+
code: "applicability-version-ambiguous",
|
|
675
|
+
severity: "warning",
|
|
676
|
+
category: "applicability",
|
|
677
|
+
message: "Actionable page discusses multiple current, legacy, or migrated versions without version applicability metadata."
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
findings,
|
|
682
|
+
conflicting,
|
|
683
|
+
ambiguous,
|
|
684
|
+
mismatched
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function analyzeCommand(options) {
|
|
688
|
+
const findings = [];
|
|
689
|
+
let verificationEstablished = false;
|
|
690
|
+
const commandText = normalizeShellCommand(options.command.run);
|
|
691
|
+
if (!commandText) return commandAnalysis(findings);
|
|
692
|
+
let commandCwd = options.command.cwd ?? ".";
|
|
693
|
+
const inlineCwd = /^(?:cd)\s+([^;&|]+?)\s*&&\s*(.+)$/s.exec(commandText);
|
|
694
|
+
const executableCommand = inlineCwd?.[2]?.trim() ?? commandText;
|
|
695
|
+
if (inlineCwd?.[1]) commandCwd = stripShellQuotes(inlineCwd[1].trim());
|
|
696
|
+
const resolvedCwd = path.resolve(options.rootDir, commandCwd);
|
|
697
|
+
if (!isPathInside(options.rootDir, resolvedCwd)) {
|
|
698
|
+
findings.push(commandFinding(options, {
|
|
699
|
+
code: "command-cwd-outside-root",
|
|
700
|
+
severity: "error",
|
|
701
|
+
message: `Command working directory resolves outside the project root: ${commandCwd}`
|
|
702
|
+
}));
|
|
703
|
+
return commandAnalysis(findings);
|
|
704
|
+
}
|
|
705
|
+
if (!existsSync(resolvedCwd)) {
|
|
706
|
+
findings.push(commandFinding(options, {
|
|
707
|
+
code: "command-cwd-missing",
|
|
708
|
+
severity: "error",
|
|
709
|
+
message: `Command working directory does not exist: ${commandCwd}`
|
|
710
|
+
}));
|
|
711
|
+
return commandAnalysis(findings);
|
|
712
|
+
}
|
|
713
|
+
if (hasUnsupportedShellControlSyntax(executableCommand)) {
|
|
714
|
+
findings.push(commandFinding(options, {
|
|
715
|
+
code: "command-unverified",
|
|
716
|
+
severity: "suggestion",
|
|
717
|
+
message: "Compound commands, redirections, and shell expansions are not executed or inferred; this command is unverified."
|
|
718
|
+
}));
|
|
719
|
+
return commandAnalysis(findings);
|
|
720
|
+
}
|
|
721
|
+
const tokens = tokenizeShellCommand(executableCommand);
|
|
722
|
+
const commandPackageManager = readCommandPackageManager(tokens);
|
|
723
|
+
const expectedPackageManager = options.command.packageManagerHint ?? (options.command.source === "contract" ? options.packageManager : void 0);
|
|
724
|
+
if (commandPackageManager && expectedPackageManager && commandPackageManager !== expectedPackageManager) findings.push(commandFinding(options, {
|
|
725
|
+
code: "command-package-manager-mismatch",
|
|
726
|
+
severity: "warning",
|
|
727
|
+
message: `Command uses ${commandPackageManager}, but ${expectedPackageManager} is expected for this project or code block.`
|
|
728
|
+
}));
|
|
729
|
+
const workspaceSelection = readWorkspaceSelection(tokens, commandPackageManager);
|
|
730
|
+
const workspaceResolution = resolveWorkspaceSelection(workspaceSelection, options.packageManifests, options.rootDir, options.workspaceRoot);
|
|
731
|
+
if (workspaceResolution.status === "unresolved") {
|
|
732
|
+
const selectors = workspaceSelection.selectors.length ? workspaceSelection.selectors.join(", ") : "all workspaces";
|
|
733
|
+
findings.push(commandFinding(options, {
|
|
734
|
+
code: "command-unverified",
|
|
735
|
+
severity: "suggestion",
|
|
736
|
+
message: `Workspace selector could not be resolved statically (${selectors}); this command is unverified.`
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
const script = readPackageScript(tokens);
|
|
740
|
+
if (script) {
|
|
741
|
+
const packageJsons = workspaceResolution.status === "resolved" ? workspaceResolution.manifests : workspaceResolution.status === "none" ? [readNearestPackageJson(resolvedCwd, options.rootDir)].filter((manifest) => Boolean(manifest)) : [];
|
|
742
|
+
if (workspaceResolution.status === "none" && packageJsons.length === 0) findings.push(commandFinding(options, {
|
|
743
|
+
code: "command-unverified",
|
|
744
|
+
severity: "suggestion",
|
|
745
|
+
message: `No package.json could be found for package script "${script}"; this command is unverified.`
|
|
746
|
+
}));
|
|
747
|
+
for (const packageJson of packageJsons) if (packageJson.scripts.has(script)) verificationEstablished = true;
|
|
748
|
+
else findings.push(commandFinding(options, {
|
|
749
|
+
code: "command-script-missing",
|
|
750
|
+
severity: "error",
|
|
751
|
+
message: `Command references package script "${script}", but that script is not defined in ${packageJson.relativePath}.`
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
const docsCommand = readDocsCommand(tokens);
|
|
755
|
+
if (docsCommand) if (matchesKnownDocsCommand(docsCommand, options.knownDocsCommands)) verificationEstablished = true;
|
|
756
|
+
else findings.push(commandFinding(options, {
|
|
757
|
+
code: "command-cli-unknown",
|
|
758
|
+
severity: "error",
|
|
759
|
+
message: `Command references an unknown docs CLI command: docs ${docsCommand}`
|
|
760
|
+
}));
|
|
761
|
+
if (isStaticallyKnownPackageManagerCommand(tokens) || isVersionProbe(tokens) || isStaticallyValidCurlCommand(tokens) || isStaticallyValidShellBuiltin(tokens, resolvedCwd, options.rootDir) || isStaticallyValidAgentToolCommand(tokens)) verificationEstablished = true;
|
|
762
|
+
if (!verificationEstablished && findings.length === 0) findings.push(commandFinding(options, {
|
|
763
|
+
code: "command-unverified",
|
|
764
|
+
severity: "suggestion",
|
|
765
|
+
message: "Command form could not be verified statically; this command is unverified."
|
|
766
|
+
}));
|
|
767
|
+
return commandAnalysis(findings);
|
|
768
|
+
}
|
|
769
|
+
function commandAnalysis(findings) {
|
|
770
|
+
if (findings.some((finding) => finding.code !== "command-unverified")) return {
|
|
771
|
+
findings,
|
|
772
|
+
status: "unhealthy"
|
|
773
|
+
};
|
|
774
|
+
if (findings.length > 0) return {
|
|
775
|
+
findings,
|
|
776
|
+
status: "unverified"
|
|
777
|
+
};
|
|
778
|
+
return {
|
|
779
|
+
findings,
|
|
780
|
+
status: "healthy"
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function collectPageCommands(page, agent, projectedSource) {
|
|
784
|
+
const commands = [];
|
|
785
|
+
for (const command of agent?.commands ?? []) commands.push(normalizeAgentCommand(command, page.sourcePath));
|
|
786
|
+
for (const verification of agent?.verification ?? []) if (typeof verification !== "string" && verification.run) commands.push({
|
|
787
|
+
run: verification.run,
|
|
788
|
+
sourcePath: page.sourcePath,
|
|
789
|
+
source: "contract"
|
|
790
|
+
});
|
|
791
|
+
collectMarkdownCommands(commands, projectedSource, page.sourcePath);
|
|
792
|
+
if (page.agentSource) collectMarkdownCommands(commands, page.agentSource, page.agentSourcePath ?? page.sourcePath);
|
|
793
|
+
return dedupeCommands(commands);
|
|
794
|
+
}
|
|
795
|
+
function collectMarkdownCommands(commands, source, sourcePath) {
|
|
796
|
+
const blocks = extractCodeBlocksFromMarkdown({
|
|
797
|
+
source,
|
|
798
|
+
filePath: sourcePath,
|
|
799
|
+
relativePath: sourcePath
|
|
800
|
+
});
|
|
801
|
+
for (const block of blocks) {
|
|
802
|
+
if (!/^(?:bash|console|sh|shell|zsh)$/i.test(block.language ?? "")) continue;
|
|
803
|
+
for (const command of shellLines(block.code, block.language)) commands.push({
|
|
804
|
+
run: command,
|
|
805
|
+
line: block.lineStart,
|
|
806
|
+
sourcePath,
|
|
807
|
+
packageManagerHint: block.packageManager,
|
|
808
|
+
source: "fence"
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function normalizeAgentCommand(command, sourcePath) {
|
|
813
|
+
return typeof command === "string" ? {
|
|
814
|
+
run: command,
|
|
815
|
+
sourcePath,
|
|
816
|
+
source: "contract"
|
|
817
|
+
} : {
|
|
818
|
+
run: command.run,
|
|
819
|
+
cwd: command.cwd,
|
|
820
|
+
sourcePath,
|
|
821
|
+
source: "contract"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function shellLines(code, language) {
|
|
825
|
+
const lines = code.replace(/\\\s*\r?\n\s*/g, " ").split(/\r?\n/);
|
|
826
|
+
const promptedConsole = language?.toLowerCase() === "console" && lines.some((line) => /^(?:\$|>)\s+/.test(line.trim()));
|
|
827
|
+
return lines.map((line) => line.trim()).filter((line) => !promptedConsole || /^(?:\$|>)\s+/.test(line)).map((line) => line.replace(/^(?:\$|>)\s+/, "")).filter((line) => Boolean(line) && !line.startsWith("#") && !/^\w+=\S+$/.test(line));
|
|
828
|
+
}
|
|
829
|
+
function dedupeCommands(commands) {
|
|
830
|
+
const seen = /* @__PURE__ */ new Set();
|
|
831
|
+
return commands.filter((command) => {
|
|
832
|
+
const key = `${command.sourcePath ?? ""}\0${command.cwd ?? ""}\0${command.run}\0${command.packageManagerHint ?? ""}`;
|
|
833
|
+
if (seen.has(key)) return false;
|
|
834
|
+
seen.add(key);
|
|
835
|
+
return true;
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
function hasExpectedResult(agent) {
|
|
839
|
+
if (agent?.outcome) return true;
|
|
840
|
+
return Boolean(agent?.verification?.some((step) => {
|
|
841
|
+
if (typeof step === "string") return EXPECTED_RESULT_PATTERN.test(step);
|
|
842
|
+
return Boolean(step.expect?.trim() || EXPECTED_RESULT_PATTERN.test(step.description ?? ""));
|
|
843
|
+
}));
|
|
844
|
+
}
|
|
845
|
+
function hasRecovery(agent) {
|
|
846
|
+
if (agent?.rollback?.length) return true;
|
|
847
|
+
return Boolean(agent?.failureModes?.some((mode) => {
|
|
848
|
+
if (typeof mode === "string") return RECOVERY_PATTERN.test(mode);
|
|
849
|
+
return Boolean(mode.resolution?.trim());
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
function splitNormalizedSentences(content) {
|
|
853
|
+
const withoutFences = stripFencedContent(content);
|
|
854
|
+
const sentences = [];
|
|
855
|
+
for (const line of withoutFences.split(/\r?\n/)) {
|
|
856
|
+
const cleanedLine = line.replace(/^\s*(?:[-*+] |\d+[.)] )/, "").replace(/^\s{0,3}#{1,6}\s+/, "").trim();
|
|
857
|
+
if (!cleanedLine) continue;
|
|
858
|
+
const parts = cleanedLine.match(/[^.!?]+[.!?]?/g) ?? [cleanedLine];
|
|
859
|
+
for (const part of parts) {
|
|
860
|
+
const normalized = normalizeSentence(part);
|
|
861
|
+
if (normalized) sentences.push(normalized);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return sentences;
|
|
865
|
+
}
|
|
866
|
+
function normalizeSentence(value) {
|
|
867
|
+
return value.replace(/!?(?:\[([^\]]*)\])\([^)]+\)/g, "$1").replace(/[`*_~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
868
|
+
}
|
|
869
|
+
function normalizeBlockSignature(content) {
|
|
870
|
+
const signature = [];
|
|
871
|
+
let fence;
|
|
872
|
+
for (const line of content.replace(/\r\n?/g, "\n").split("\n")) {
|
|
873
|
+
const trimmed = line.trim();
|
|
874
|
+
const nextFence = advanceFence(trimmed, fence);
|
|
875
|
+
if (fence) {
|
|
876
|
+
if (nextFence) signature.push(`code:${line}`);
|
|
877
|
+
else signature.push("fence:end");
|
|
878
|
+
fence = nextFence;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (nextFence) {
|
|
882
|
+
signature.push(`fence:start:${trimmed}`);
|
|
883
|
+
fence = nextFence;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const prose = normalizeSentence(line);
|
|
887
|
+
if (prose) signature.push(`prose:${prose}`);
|
|
888
|
+
}
|
|
889
|
+
return signature.join("\n");
|
|
890
|
+
}
|
|
891
|
+
function stripFencedContent(source) {
|
|
892
|
+
const output = [];
|
|
893
|
+
let fence;
|
|
894
|
+
for (const line of source.split(/\r?\n/)) {
|
|
895
|
+
const nextFence = advanceFence(line.trim(), fence);
|
|
896
|
+
if (!fence && !nextFence) output.push(line);
|
|
897
|
+
fence = nextFence;
|
|
898
|
+
}
|
|
899
|
+
return output.join("\n");
|
|
900
|
+
}
|
|
901
|
+
function advanceFence(line, fence) {
|
|
902
|
+
if (fence) {
|
|
903
|
+
const closing = /^(`+|~+)\s*$/.exec(line)?.[1];
|
|
904
|
+
if (closing?.[0] === fence.character && closing.length >= fence.length) return void 0;
|
|
905
|
+
return fence;
|
|
906
|
+
}
|
|
907
|
+
const opening = /^(`{3,}|~{3,})(.*)$/.exec(line)?.[1];
|
|
908
|
+
if (!opening) return void 0;
|
|
909
|
+
return {
|
|
910
|
+
character: opening[0],
|
|
911
|
+
length: opening.length
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function isGenericAgentBlock(content, sentences) {
|
|
915
|
+
const words = normalizeSentence(content).match(/[a-z0-9][a-z0-9_-]*/g) ?? [];
|
|
916
|
+
const hasSpecificContext = hasMeaningfulSpecificContext(content);
|
|
917
|
+
const genericSentences = sentences.filter((sentence) => GENERIC_SENTENCE_PATTERN.test(sentence));
|
|
918
|
+
const totalSentenceChars = sentences.reduce((total, sentence) => total + sentence.length, 0);
|
|
919
|
+
const genericSentenceChars = genericSentences.reduce((total, sentence) => total + sentence.length, 0);
|
|
920
|
+
const genericRatio = totalSentenceChars > 0 ? genericSentenceChars / totalSentenceChars : 0;
|
|
921
|
+
return !hasSpecificContext && words.length < 12 || !hasSpecificContext && sentences.length > 0 && genericRatio >= .67;
|
|
922
|
+
}
|
|
923
|
+
function hasMeaningfulSpecificContext(content) {
|
|
924
|
+
if (STRONG_SPECIFIC_CONTEXT_PATTERN.test(content)) return true;
|
|
925
|
+
if (!ACTIONABLE_PATTERN.test(content)) return false;
|
|
926
|
+
if (/https?:\/\/[^\s)]+/i.test(content)) return true;
|
|
927
|
+
const trivialInlineValues = new Set([
|
|
928
|
+
"config",
|
|
929
|
+
"documentation",
|
|
930
|
+
"docs",
|
|
931
|
+
"page",
|
|
932
|
+
"settings"
|
|
933
|
+
]);
|
|
934
|
+
return [...content.matchAll(/`([^`\n]+)`/g)].some((match) => {
|
|
935
|
+
const value = (match[1] ?? "").trim().toLowerCase();
|
|
936
|
+
return value.length >= 3 && !trivialInlineValues.has(value);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function normalizeFrameworks(value) {
|
|
940
|
+
return normalizeValues(value).map(normalizeAgentFramework);
|
|
941
|
+
}
|
|
942
|
+
function normalizeValues(value) {
|
|
943
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
944
|
+
return Array.from(new Set(values.map((item) => item.trim().toLowerCase()).filter((item) => item.length > 0)));
|
|
945
|
+
}
|
|
946
|
+
function inferFrameworks(source) {
|
|
947
|
+
return FRAMEWORK_PATTERNS.filter(([, pattern]) => pattern.test(source)).map(([name]) => name);
|
|
948
|
+
}
|
|
949
|
+
function hasAmbiguousVersionSignals(source) {
|
|
950
|
+
if (!/\b(?:current|deprecated|latest|legacy|migrat(?:e|ion)|version)\b/i.test(source)) return false;
|
|
951
|
+
return new Set([...source.matchAll(/\b(?:v|version\s*)(\d+(?:\.\d+){0,2})\b/gi)].map((match) => match[1])).size > 1;
|
|
952
|
+
}
|
|
953
|
+
function versionSetsCompatible(left, right) {
|
|
954
|
+
return left.some((leftVersion) => right.some((rightVersion) => versionsCompatible(leftVersion, rightVersion)));
|
|
955
|
+
}
|
|
956
|
+
function versionsCompatible(left, right) {
|
|
957
|
+
const normalizedLeft = normalizeAgentVersion(left);
|
|
958
|
+
const normalizedRight = normalizeAgentVersion(right);
|
|
959
|
+
if (normalizedLeft === normalizedRight) return true;
|
|
960
|
+
if (isVersionConstraint(normalizedLeft) && isVersionConstraint(normalizedRight)) return agentVersionConstraintsOverlap(normalizedLeft, normalizedRight);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
function isExactVersion(value) {
|
|
964
|
+
return /^\d+(?:\.\d+){0,2}(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i.test(value);
|
|
965
|
+
}
|
|
966
|
+
function isVersionConstraint(value) {
|
|
967
|
+
return isExactVersion(value) || /(?:[<>=~^*x]|\|\||\s+-\s+)/i.test(value);
|
|
968
|
+
}
|
|
969
|
+
function setsOverlap(left, right) {
|
|
970
|
+
return left.some((value) => right.includes(value));
|
|
971
|
+
}
|
|
972
|
+
function normalizeRelated(related) {
|
|
973
|
+
if (!related) return [];
|
|
974
|
+
return Array.from(new Set(related.map((item) => typeof item === "string" ? item : item.href).map((item) => item.trim()).filter(Boolean)));
|
|
975
|
+
}
|
|
976
|
+
function internalRelatedRoute(href) {
|
|
977
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#")) return void 0;
|
|
978
|
+
const pathname = href.split(/[?#]/, 1)[0];
|
|
979
|
+
if (!pathname) return void 0;
|
|
980
|
+
return normalizeRoute(pathname);
|
|
981
|
+
}
|
|
982
|
+
function normalizeRoute(route) {
|
|
983
|
+
const normalized = `/${route}`.replace(/\/+/g, "/").replace(/\.md$/i, "");
|
|
984
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
985
|
+
}
|
|
986
|
+
function detectPackageManager(rootDir) {
|
|
987
|
+
if (existsSync(path.join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
988
|
+
if (existsSync(path.join(rootDir, "yarn.lock"))) return "yarn";
|
|
989
|
+
if (existsSync(path.join(rootDir, "bun.lock")) || existsSync(path.join(rootDir, "bun.lockb"))) return "bun";
|
|
990
|
+
if (existsSync(path.join(rootDir, "package-lock.json"))) return "npm";
|
|
991
|
+
const packageJson = readJsonFile(path.join(rootDir, "package.json"));
|
|
992
|
+
const declared = typeof packageJson?.packageManager === "string" ? packageJson.packageManager : "";
|
|
993
|
+
return /^(npm|pnpm|yarn|bun)@/.exec(declared)?.[1];
|
|
994
|
+
}
|
|
995
|
+
function findEnclosingWorkspaceRoot(startDir) {
|
|
996
|
+
const repositoryBoundary = findNearestGitBoundary(startDir);
|
|
997
|
+
let current = startDir;
|
|
998
|
+
let traversed = 0;
|
|
999
|
+
while (true) {
|
|
1000
|
+
if (existsSync(path.join(current, "pnpm-workspace.yaml")) || existsSync(path.join(current, "pnpm-workspace.yml"))) return current;
|
|
1001
|
+
const workspaces = readJsonFile(path.join(current, "package.json"))?.workspaces;
|
|
1002
|
+
if (Array.isArray(workspaces) || workspaces !== null && typeof workspaces === "object" && !Array.isArray(workspaces)) return current;
|
|
1003
|
+
if (current === repositoryBoundary || !repositoryBoundary && traversed >= 12) return startDir;
|
|
1004
|
+
const parent = path.dirname(current);
|
|
1005
|
+
if (parent === current) return startDir;
|
|
1006
|
+
current = parent;
|
|
1007
|
+
traversed += 1;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function findNearestGitBoundary(startDir) {
|
|
1011
|
+
let current = startDir;
|
|
1012
|
+
while (true) {
|
|
1013
|
+
if (existsSync(path.join(current, ".git"))) return current;
|
|
1014
|
+
const parent = path.dirname(current);
|
|
1015
|
+
if (parent === current) return void 0;
|
|
1016
|
+
current = parent;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function normalizeShellCommand(command) {
|
|
1020
|
+
return command.trim().replace(/^\$\s*/, "");
|
|
1021
|
+
}
|
|
1022
|
+
function hasUnsupportedShellControlSyntax(command) {
|
|
1023
|
+
let quote;
|
|
1024
|
+
let escaped = false;
|
|
1025
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
1026
|
+
const character = command[index] ?? "";
|
|
1027
|
+
if (escaped) {
|
|
1028
|
+
escaped = false;
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (character === "\\") {
|
|
1032
|
+
escaped = true;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (quote) {
|
|
1036
|
+
if (character === quote) quote = void 0;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (character === "'" || character === "\"") {
|
|
1040
|
+
quote = character;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (character === "<") {
|
|
1044
|
+
const placeholder = /^<[A-Za-z0-9._-]+>/.exec(command.slice(index))?.[0];
|
|
1045
|
+
if (placeholder) {
|
|
1046
|
+
index += placeholder.length - 1;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if ([
|
|
1051
|
+
"&",
|
|
1052
|
+
"|",
|
|
1053
|
+
";",
|
|
1054
|
+
"<",
|
|
1055
|
+
">",
|
|
1056
|
+
"`",
|
|
1057
|
+
"\n",
|
|
1058
|
+
"\r"
|
|
1059
|
+
].includes(character) || character === "$" && command[index + 1] === "(") return true;
|
|
1060
|
+
}
|
|
1061
|
+
return Boolean(quote || escaped);
|
|
1062
|
+
}
|
|
1063
|
+
function tokenizeShellCommand(command) {
|
|
1064
|
+
return command.match(/"[^"]*"|'[^']*'|[^\s]+/g)?.map(stripShellQuotes) ?? [];
|
|
1065
|
+
}
|
|
1066
|
+
function stripShellQuotes(value) {
|
|
1067
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
1068
|
+
return value;
|
|
1069
|
+
}
|
|
1070
|
+
function readCommandPackageManager(tokens) {
|
|
1071
|
+
const command = tokens[0];
|
|
1072
|
+
if (command === "npx") return "npm";
|
|
1073
|
+
if (command === "pnpx") return "pnpm";
|
|
1074
|
+
if (command === "bunx") return "bun";
|
|
1075
|
+
return command && command in PACKAGE_MANAGER_BUILT_INS ? command : void 0;
|
|
1076
|
+
}
|
|
1077
|
+
function readPackageScript(tokens) {
|
|
1078
|
+
const manager = readCommandPackageManager(tokens);
|
|
1079
|
+
if (!manager) return void 0;
|
|
1080
|
+
if ([
|
|
1081
|
+
"npx",
|
|
1082
|
+
"pnpx",
|
|
1083
|
+
"bunx"
|
|
1084
|
+
].includes(tokens[0] ?? "")) return void 0;
|
|
1085
|
+
if (manager === "yarn") {
|
|
1086
|
+
const workspaceIndex = findPackageManagerCommandIndex(tokens);
|
|
1087
|
+
if (workspaceIndex >= 0 && tokens[workspaceIndex] === "workspace") return readScriptFromArguments(manager, tokens.slice(workspaceIndex + 2));
|
|
1088
|
+
}
|
|
1089
|
+
return readScriptFromArguments(manager, readPackageManagerArguments(tokens));
|
|
1090
|
+
}
|
|
1091
|
+
function readScriptFromArguments(manager, commandArguments) {
|
|
1092
|
+
if ([
|
|
1093
|
+
"dlx",
|
|
1094
|
+
"exec",
|
|
1095
|
+
"x"
|
|
1096
|
+
].includes(commandArguments[0] ?? "")) return void 0;
|
|
1097
|
+
if (["run", "run-script"].includes(commandArguments[0] ?? "")) return cleanShellToken(commandArguments[1]);
|
|
1098
|
+
const candidate = cleanShellToken(commandArguments[0]);
|
|
1099
|
+
if (!candidate) return void 0;
|
|
1100
|
+
if ([
|
|
1101
|
+
"start",
|
|
1102
|
+
"stop",
|
|
1103
|
+
"restart",
|
|
1104
|
+
"test"
|
|
1105
|
+
].includes(candidate)) return candidate;
|
|
1106
|
+
return PACKAGE_MANAGER_BUILT_INS[manager]?.has(candidate) ? void 0 : candidate;
|
|
1107
|
+
}
|
|
1108
|
+
function readWorkspaceSelection(tokens, manager) {
|
|
1109
|
+
if (!manager) return {
|
|
1110
|
+
requested: false,
|
|
1111
|
+
selectors: []
|
|
1112
|
+
};
|
|
1113
|
+
if (manager === "pnpm" || manager === "bun") {
|
|
1114
|
+
const selectors = readOptionValues(tokens, ["--filter"], manager === "pnpm" ? ["-F"] : []);
|
|
1115
|
+
const recursive = manager === "pnpm" && hasOptionBeforeCommand(tokens, ["-r", "--recursive"]);
|
|
1116
|
+
return {
|
|
1117
|
+
requested: selectors.length > 0 || recursive,
|
|
1118
|
+
selectors
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
if (manager === "npm") {
|
|
1122
|
+
const selectors = readOptionValues(tokens, ["--workspace"], ["-w"]);
|
|
1123
|
+
const allWorkspaces = hasOptionBeforeCommand(tokens, ["--workspaces"]);
|
|
1124
|
+
return {
|
|
1125
|
+
requested: selectors.length > 0 || allWorkspaces,
|
|
1126
|
+
selectors
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
if (manager === "yarn") {
|
|
1130
|
+
const workspaceIndex = findPackageManagerCommandIndex(tokens);
|
|
1131
|
+
if (workspaceIndex >= 0 && tokens[workspaceIndex] === "workspace") {
|
|
1132
|
+
const selector = cleanShellToken(tokens[workspaceIndex + 1]);
|
|
1133
|
+
return {
|
|
1134
|
+
requested: true,
|
|
1135
|
+
selectors: selector ? [selector] : []
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
if (workspaceIndex >= 0 && tokens[workspaceIndex] === "workspaces") return {
|
|
1139
|
+
requested: true,
|
|
1140
|
+
selectors: []
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
requested: false,
|
|
1145
|
+
selectors: []
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function readOptionValues(tokens, longOptions, shortOptions) {
|
|
1149
|
+
const values = [];
|
|
1150
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1151
|
+
const token = tokens[index] ?? "";
|
|
1152
|
+
if (token === "--" || !token.startsWith("-")) break;
|
|
1153
|
+
if ([...longOptions, ...shortOptions].includes(token)) {
|
|
1154
|
+
const value = cleanShellToken(tokens[index + 1]);
|
|
1155
|
+
if (value) values.push(value);
|
|
1156
|
+
index += 1;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
const longOption = longOptions.find((option) => token.startsWith(`${option}=`));
|
|
1160
|
+
if (longOption) {
|
|
1161
|
+
const value = cleanShellToken(token.slice(longOption.length + 1));
|
|
1162
|
+
if (value) values.push(value);
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (PACKAGE_MANAGER_OPTIONS_WITH_VALUES.has(token)) {
|
|
1166
|
+
index += 1;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const shortOption = shortOptions.find((option) => token.startsWith(option) && token.length > option.length);
|
|
1170
|
+
if (shortOption) {
|
|
1171
|
+
const value = cleanShellToken(token.slice(shortOption.length).replace(/^=/, ""));
|
|
1172
|
+
if (value) values.push(value);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return values;
|
|
1176
|
+
}
|
|
1177
|
+
function readPackageManagerArguments(tokens) {
|
|
1178
|
+
const commandArguments = [];
|
|
1179
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1180
|
+
const token = tokens[index] ?? "";
|
|
1181
|
+
if (token === "--") break;
|
|
1182
|
+
if (PACKAGE_MANAGER_OPTIONS_WITH_VALUES.has(token)) {
|
|
1183
|
+
index += 1;
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (token.startsWith("-")) continue;
|
|
1187
|
+
commandArguments.push(token);
|
|
1188
|
+
}
|
|
1189
|
+
return commandArguments;
|
|
1190
|
+
}
|
|
1191
|
+
function findPackageManagerCommandIndex(tokens) {
|
|
1192
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1193
|
+
const token = tokens[index] ?? "";
|
|
1194
|
+
if (token === "--") return -1;
|
|
1195
|
+
if (PACKAGE_MANAGER_OPTIONS_WITH_VALUES.has(token)) {
|
|
1196
|
+
index += 1;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (!token.startsWith("-")) return index;
|
|
1200
|
+
}
|
|
1201
|
+
return -1;
|
|
1202
|
+
}
|
|
1203
|
+
function hasOptionBeforeCommand(tokens, options) {
|
|
1204
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1205
|
+
const token = tokens[index] ?? "";
|
|
1206
|
+
if (token === "--" || !token.startsWith("-")) return false;
|
|
1207
|
+
if (options.includes(token)) return true;
|
|
1208
|
+
if (PACKAGE_MANAGER_OPTIONS_WITH_VALUES.has(token)) index += 1;
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
function isStaticallyKnownPackageManagerCommand(tokens) {
|
|
1213
|
+
const manager = readCommandPackageManager(tokens);
|
|
1214
|
+
if (!manager || [
|
|
1215
|
+
"npx",
|
|
1216
|
+
"pnpx",
|
|
1217
|
+
"bunx"
|
|
1218
|
+
].includes(tokens[0] ?? "")) return false;
|
|
1219
|
+
const command = readPackageManagerArguments(tokens)[0];
|
|
1220
|
+
if (!command || [
|
|
1221
|
+
"dlx",
|
|
1222
|
+
"exec",
|
|
1223
|
+
"x"
|
|
1224
|
+
].includes(command)) return false;
|
|
1225
|
+
return Boolean(PACKAGE_MANAGER_BUILT_INS[manager]?.has(command));
|
|
1226
|
+
}
|
|
1227
|
+
function isVersionProbe(tokens) {
|
|
1228
|
+
return tokens.length === 2 && [
|
|
1229
|
+
"node",
|
|
1230
|
+
"deno",
|
|
1231
|
+
"bun",
|
|
1232
|
+
"npm",
|
|
1233
|
+
"pnpm",
|
|
1234
|
+
"yarn"
|
|
1235
|
+
].includes(tokens[0] ?? "") && ["--version", "-v"].includes(tokens[1] ?? "");
|
|
1236
|
+
}
|
|
1237
|
+
function isStaticallyValidCurlCommand(tokens) {
|
|
1238
|
+
if (tokens[0] !== "curl") return false;
|
|
1239
|
+
let urls = 0;
|
|
1240
|
+
let parseOptions = true;
|
|
1241
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1242
|
+
const token = tokens[index] ?? "";
|
|
1243
|
+
if (parseOptions && token === "--") {
|
|
1244
|
+
parseOptions = false;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (parseOptions && token.startsWith("--")) {
|
|
1248
|
+
const equalsIndex = token.indexOf("=");
|
|
1249
|
+
const option = equalsIndex >= 0 ? token.slice(0, equalsIndex) : token;
|
|
1250
|
+
const inlineValue = equalsIndex >= 0 ? token.slice(equalsIndex + 1) : void 0;
|
|
1251
|
+
if (CURL_OPTIONS_WITHOUT_VALUES.has(option)) {
|
|
1252
|
+
if (inlineValue !== void 0) return false;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (!CURL_OPTIONS_WITH_VALUES.has(option)) return false;
|
|
1256
|
+
const value = inlineValue ?? tokens[index + 1];
|
|
1257
|
+
if (!value || !inlineValue && value.startsWith("-")) return false;
|
|
1258
|
+
if (inlineValue === void 0) index += 1;
|
|
1259
|
+
if (!isValidCurlOptionValue(option, value)) return false;
|
|
1260
|
+
if (option === "--url") urls += 1;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
if (parseOptions && token.startsWith("-") && token !== "-") {
|
|
1264
|
+
const shortOptions = token.slice(1);
|
|
1265
|
+
if (!shortOptions) return false;
|
|
1266
|
+
let consumed = false;
|
|
1267
|
+
for (let offset = 0; offset < shortOptions.length; offset += 1) {
|
|
1268
|
+
const option = shortOptions[offset] ?? "";
|
|
1269
|
+
if (CURL_SHORT_OPTIONS_WITHOUT_VALUES.has(option)) continue;
|
|
1270
|
+
if (!CURL_SHORT_OPTIONS_WITH_VALUES.has(option)) return false;
|
|
1271
|
+
const inlineValue = shortOptions.slice(offset + 1);
|
|
1272
|
+
const value = inlineValue || tokens[index + 1];
|
|
1273
|
+
if (!value || !inlineValue && value.startsWith("-")) return false;
|
|
1274
|
+
if (!inlineValue) index += 1;
|
|
1275
|
+
if (!isValidCurlOptionValue(`-${option}`, value)) return false;
|
|
1276
|
+
consumed = true;
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
if (consumed || [...shortOptions].every((item) => CURL_SHORT_OPTIONS_WITHOUT_VALUES.has(item))) continue;
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
if (!isHttpUrl(token)) return false;
|
|
1283
|
+
urls += 1;
|
|
1284
|
+
}
|
|
1285
|
+
return urls > 0;
|
|
1286
|
+
}
|
|
1287
|
+
function isValidCurlOptionValue(option, value) {
|
|
1288
|
+
if (option === "--url") return isHttpUrl(value);
|
|
1289
|
+
if (option === "--request" || option === "-X") return /^[A-Z]+$/i.test(value);
|
|
1290
|
+
if (option === "--header" || option === "-H") return /^[^:\s][^:]*:\s*.*$/.test(value);
|
|
1291
|
+
if (option === "--connect-timeout" || option === "--max-time") return /^\d+(?:\.\d+)?$/.test(value);
|
|
1292
|
+
if (option === "--retry" || option === "--retry-delay") return /^\d+$/.test(value);
|
|
1293
|
+
return value.length > 0;
|
|
1294
|
+
}
|
|
1295
|
+
function isHttpUrl(value) {
|
|
1296
|
+
try {
|
|
1297
|
+
const url = new URL(value);
|
|
1298
|
+
return (url.protocol === "http:" || url.protocol === "https:") && Boolean(url.hostname);
|
|
1299
|
+
} catch {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function isStaticallyValidShellBuiltin(tokens, cwd, rootDir) {
|
|
1304
|
+
const command = tokens[0];
|
|
1305
|
+
const args = tokens.slice(1);
|
|
1306
|
+
if (command === "echo") return true;
|
|
1307
|
+
if (command === "printf") return args.length > 0;
|
|
1308
|
+
if (command === "pwd") return args.every((argument) => argument === "-L" || argument === "-P");
|
|
1309
|
+
if (command === "export" || command === "unset") {
|
|
1310
|
+
if (args.length === 0) return false;
|
|
1311
|
+
return args.every((argument) => command === "export" ? /^[A-Za-z_][A-Za-z0-9_]*(?:=.*)?$/.test(argument) : /^[A-Za-z_][A-Za-z0-9_]*$/.test(argument));
|
|
1312
|
+
}
|
|
1313
|
+
if (command === "test") return isValidTestExpression(args);
|
|
1314
|
+
if (command === "[") return args.at(-1) === "]" && isValidTestExpression(args.slice(0, -1));
|
|
1315
|
+
if (command !== "cd" || args.length !== 1) return false;
|
|
1316
|
+
const target = args[0] ?? "";
|
|
1317
|
+
if (!target || target === "-" || path.isAbsolute(target) || /[$*?[\]{}]/.test(target)) return false;
|
|
1318
|
+
return isPathInside(rootDir, path.resolve(cwd, target));
|
|
1319
|
+
}
|
|
1320
|
+
function isValidTestExpression(args) {
|
|
1321
|
+
if (args[0] === "!") return args.length > 1 && isValidTestExpression(args.slice(1));
|
|
1322
|
+
if (args.length === 1) return true;
|
|
1323
|
+
if (args.length === 2) return TEST_UNARY_OPERATORS.has(args[0] ?? "");
|
|
1324
|
+
return args.length === 3 && TEST_BINARY_OPERATORS.has(args[1] ?? "");
|
|
1325
|
+
}
|
|
1326
|
+
function isStaticallyValidAgentToolCommand(tokens) {
|
|
1327
|
+
return isStaticallyValidSkillsCommand(tokens) || isStaticallyValidClaudeMcpCommand(tokens);
|
|
1328
|
+
}
|
|
1329
|
+
function isStaticallyValidSkillsCommand(tokens) {
|
|
1330
|
+
const invocation = unwrapDownloadRunner(tokens);
|
|
1331
|
+
if (!invocation || !/^skills(?:@(?:latest|\d+(?:\.\d+){0,2}))?$/.test(invocation[0] ?? "")) return false;
|
|
1332
|
+
if (invocation.length !== 3 || invocation[1] !== "add") return false;
|
|
1333
|
+
const source = invocation[2] ?? "";
|
|
1334
|
+
if (/^https:\/\/(?:www\.)?github\.com\/[^/\s]+\/[^/\s]+(?:\.git)?(?:#[^\s]+)?$/.test(source)) return true;
|
|
1335
|
+
return /^@?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#[A-Za-z0-9._/-]+)?$/.test(source);
|
|
1336
|
+
}
|
|
1337
|
+
function unwrapDownloadRunner(tokens) {
|
|
1338
|
+
const runner = tokens[0];
|
|
1339
|
+
if (runner === "skills") return tokens;
|
|
1340
|
+
if (runner === "npx" || runner === "pnpx" || runner === "bunx") return tokens.slice(1);
|
|
1341
|
+
if (runner === "npm" && tokens[1] === "exec" && tokens[2] === "--") return tokens.slice(3);
|
|
1342
|
+
if ((runner === "pnpm" || runner === "yarn" || runner === "bun") && ["dlx", "x"].includes(tokens[1] ?? "")) return tokens.slice(2);
|
|
1343
|
+
}
|
|
1344
|
+
function isStaticallyValidClaudeMcpCommand(tokens) {
|
|
1345
|
+
if (tokens.length !== 5 || tokens[0] !== "claude" || tokens[1] !== "mcp" || tokens[2] !== "add-json" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(tokens[3] ?? "")) return false;
|
|
1346
|
+
try {
|
|
1347
|
+
const config = JSON.parse(tokens[4] ?? "");
|
|
1348
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return false;
|
|
1349
|
+
if (config.type === "http" || config.type === "sse") return typeof config.url === "string" && isHttpUrl(config.url);
|
|
1350
|
+
if (config.type !== "stdio" || typeof config.command !== "string" || !config.command.trim()) return false;
|
|
1351
|
+
return config.args === void 0 || Array.isArray(config.args) && config.args.every((argument) => typeof argument === "string");
|
|
1352
|
+
} catch {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function resolveWorkspaceSelection(selection, manifests, rootDir, workspaceRoot) {
|
|
1357
|
+
if (!selection.requested) return {
|
|
1358
|
+
status: "none",
|
|
1359
|
+
manifests: []
|
|
1360
|
+
};
|
|
1361
|
+
if (selection.selectors.length === 0) return {
|
|
1362
|
+
status: "unresolved",
|
|
1363
|
+
manifests: []
|
|
1364
|
+
};
|
|
1365
|
+
const resolved = [];
|
|
1366
|
+
for (const rawSelector of selection.selectors) {
|
|
1367
|
+
const selector = rawSelector.trim();
|
|
1368
|
+
if (!selector || selector.startsWith("!") || /(?:\.\.\.|[?*[\]{}<>])/.test(selector)) return {
|
|
1369
|
+
status: "unresolved",
|
|
1370
|
+
manifests: []
|
|
1371
|
+
};
|
|
1372
|
+
const namedManifest = manifests.get(selector);
|
|
1373
|
+
if (namedManifest) {
|
|
1374
|
+
resolved.push(namedManifest);
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const selectorPaths = Array.from(new Set([path.resolve(rootDir, selector), path.resolve(workspaceRoot, selector)]));
|
|
1378
|
+
const pathManifest = [...manifests.values()].find((manifest) => selectorPaths.includes(path.resolve(manifest.directory)));
|
|
1379
|
+
if (!pathManifest) return {
|
|
1380
|
+
status: "unresolved",
|
|
1381
|
+
manifests: []
|
|
1382
|
+
};
|
|
1383
|
+
resolved.push(pathManifest);
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
status: "resolved",
|
|
1387
|
+
manifests: Array.from(new Set(resolved))
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
function cleanShellToken(value) {
|
|
1391
|
+
return value?.replace(/[;&|]+$/, "").trim() || void 0;
|
|
1392
|
+
}
|
|
1393
|
+
function readDocsCommand(tokens) {
|
|
1394
|
+
const first = tokens[0] ?? "";
|
|
1395
|
+
const isDocsBinary = (token) => token === "docs" || !token.startsWith("@") && /(?:^|\/)\.?(?:bin\/)?docs$/.test(token);
|
|
1396
|
+
let commandStart = -1;
|
|
1397
|
+
if (isDocsBinary(first)) commandStart = 1;
|
|
1398
|
+
else if ([
|
|
1399
|
+
"npx",
|
|
1400
|
+
"pnpx",
|
|
1401
|
+
"bunx"
|
|
1402
|
+
].includes(first) && tokens[1] === "@farming-labs/docs") commandStart = 2;
|
|
1403
|
+
else if ([
|
|
1404
|
+
"npm",
|
|
1405
|
+
"pnpm",
|
|
1406
|
+
"yarn",
|
|
1407
|
+
"bun"
|
|
1408
|
+
].includes(first)) {
|
|
1409
|
+
const launcherIndex = tokens.findIndex((token, index) => index > 0 && [
|
|
1410
|
+
"dlx",
|
|
1411
|
+
"exec",
|
|
1412
|
+
"x"
|
|
1413
|
+
].includes(token));
|
|
1414
|
+
if (launcherIndex >= 0) {
|
|
1415
|
+
const binaryIndex = tokens.findIndex((token, index) => index > launcherIndex && (token === "@farming-labs/docs" || isDocsBinary(token)));
|
|
1416
|
+
if (binaryIndex >= 0) commandStart = binaryIndex + 1;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (commandStart < 0) return void 0;
|
|
1420
|
+
return tokens.slice(commandStart).filter((token) => !token.startsWith("-")).slice(0, 2).map((token) => cleanShellToken(token)).filter((token) => Boolean(token)).join(" ");
|
|
1421
|
+
}
|
|
1422
|
+
function matchesKnownDocsCommand(command, known) {
|
|
1423
|
+
if (!command) return true;
|
|
1424
|
+
if (known.has(command)) return true;
|
|
1425
|
+
const first = command.split(" ")[0];
|
|
1426
|
+
return known.has(first);
|
|
1427
|
+
}
|
|
1428
|
+
function readNearestPackageJson(startDir, rootDir) {
|
|
1429
|
+
let current = startDir;
|
|
1430
|
+
while (isPathInside(rootDir, current)) {
|
|
1431
|
+
const packagePath = path.join(current, "package.json");
|
|
1432
|
+
const contents = readJsonFile(packagePath);
|
|
1433
|
+
if (contents) {
|
|
1434
|
+
const scripts = contents.scripts && typeof contents.scripts === "object" && !Array.isArray(contents.scripts) ? new Set(Object.keys(contents.scripts)) : /* @__PURE__ */ new Set();
|
|
1435
|
+
return {
|
|
1436
|
+
directory: current,
|
|
1437
|
+
scripts,
|
|
1438
|
+
relativePath: path.relative(rootDir, packagePath).replace(/\\/g, "/") || "package.json"
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
if (current === rootDir) break;
|
|
1442
|
+
current = path.dirname(current);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function readProjectPackageManifests(rootDir) {
|
|
1446
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
1447
|
+
const ignored = new Set([
|
|
1448
|
+
".git",
|
|
1449
|
+
".next",
|
|
1450
|
+
".nuxt",
|
|
1451
|
+
".output",
|
|
1452
|
+
".svelte-kit",
|
|
1453
|
+
"build",
|
|
1454
|
+
"coverage",
|
|
1455
|
+
"dist",
|
|
1456
|
+
"node_modules",
|
|
1457
|
+
"out"
|
|
1458
|
+
]);
|
|
1459
|
+
const visit = (directory) => {
|
|
1460
|
+
const packagePath = path.join(directory, "package.json");
|
|
1461
|
+
const contents = readJsonFile(packagePath);
|
|
1462
|
+
if (contents) {
|
|
1463
|
+
const name = typeof contents.name === "string" ? contents.name.trim() : "";
|
|
1464
|
+
const scripts = contents.scripts && typeof contents.scripts === "object" && !Array.isArray(contents.scripts) ? new Set(Object.keys(contents.scripts)) : /* @__PURE__ */ new Set();
|
|
1465
|
+
if (name) manifests.set(name, {
|
|
1466
|
+
directory,
|
|
1467
|
+
scripts,
|
|
1468
|
+
relativePath: path.relative(rootDir, packagePath).replace(/\\/g, "/") || "package.json"
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
let names;
|
|
1472
|
+
try {
|
|
1473
|
+
names = readdirSync(directory);
|
|
1474
|
+
} catch {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
for (const name of names) {
|
|
1478
|
+
if (ignored.has(name)) continue;
|
|
1479
|
+
const child = path.join(directory, name);
|
|
1480
|
+
try {
|
|
1481
|
+
if (lstatSync(child).isDirectory()) visit(child);
|
|
1482
|
+
} catch {}
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
visit(rootDir);
|
|
1486
|
+
return manifests;
|
|
1487
|
+
}
|
|
1488
|
+
function readJsonFile(filePath) {
|
|
1489
|
+
if (!existsSync(filePath)) return void 0;
|
|
1490
|
+
try {
|
|
1491
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
1492
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function isPathInside(rootDir, candidate) {
|
|
1498
|
+
const relative = path.relative(rootDir, candidate);
|
|
1499
|
+
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1500
|
+
}
|
|
1501
|
+
function commandFinding(options, finding) {
|
|
1502
|
+
return {
|
|
1503
|
+
...makeFinding(options.page, {
|
|
1504
|
+
...finding,
|
|
1505
|
+
category: "command",
|
|
1506
|
+
line: options.command.line,
|
|
1507
|
+
command: options.command.run
|
|
1508
|
+
}),
|
|
1509
|
+
file: options.command.sourcePath ?? options.page.sourcePath
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function makeFinding(page, finding) {
|
|
1513
|
+
return {
|
|
1514
|
+
...finding,
|
|
1515
|
+
file: page.sourcePath,
|
|
1516
|
+
route: page.route
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function blockKey(block, index) {
|
|
1520
|
+
return `${block.sourcePath}:${block.line}:${index}`;
|
|
1521
|
+
}
|
|
1522
|
+
function compareFindings(left, right) {
|
|
1523
|
+
return left.file.localeCompare(right.file) || (left.line ?? 0) - (right.line ?? 0) || left.code.localeCompare(right.code) || left.message.localeCompare(right.message);
|
|
1524
|
+
}
|
|
1525
|
+
function percentage(numerator, denominator) {
|
|
1526
|
+
if (denominator === 0) return 0;
|
|
1527
|
+
return Math.round(numerator / denominator * 100);
|
|
1528
|
+
}
|
|
1529
|
+
function clampRatio(value) {
|
|
1530
|
+
if (!Number.isFinite(value)) return 0;
|
|
1531
|
+
return Math.min(1, Math.max(0, value));
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
//#endregion
|
|
1535
|
+
//#region src/agent-surface-drift.ts
|
|
1536
|
+
function isRecord(value) {
|
|
1537
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1538
|
+
}
|
|
1539
|
+
function normalizeDotPath(value) {
|
|
1540
|
+
const segments = value.trim().replace(/\[\d+\]/gu, "[]").replace(/^\/+|\/+$/g, "").replaceAll("/", ".").split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
1541
|
+
const normalized = [];
|
|
1542
|
+
for (const segment of segments) {
|
|
1543
|
+
if (/^\d+$/u.test(segment) && normalized.length > 0) {
|
|
1544
|
+
normalized[normalized.length - 1] = `${normalized.at(-1)}[]`;
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
normalized.push(segment);
|
|
1548
|
+
}
|
|
1549
|
+
return normalized.join(".");
|
|
1550
|
+
}
|
|
1551
|
+
function collectSchemaPaths(options) {
|
|
1552
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1553
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1554
|
+
const visit = (option) => {
|
|
1555
|
+
if (visited.has(option)) return;
|
|
1556
|
+
visited.add(option);
|
|
1557
|
+
const optionPath = normalizeDotPath(option.path);
|
|
1558
|
+
if (optionPath) paths.add(optionPath);
|
|
1559
|
+
for (const child of option.children ?? []) visit(child);
|
|
1560
|
+
};
|
|
1561
|
+
for (const option of options) visit(option);
|
|
1562
|
+
return paths;
|
|
1563
|
+
}
|
|
1564
|
+
function schemaCoversConfigPath(schemaPaths, configPath) {
|
|
1565
|
+
if (schemaPaths.has(configPath)) return true;
|
|
1566
|
+
if (configPath.endsWith("[]") && schemaPaths.has(configPath.slice(0, -2))) return true;
|
|
1567
|
+
for (const schemaPath of schemaPaths) {
|
|
1568
|
+
if (!schemaPath.endsWith(".*")) continue;
|
|
1569
|
+
const prefix = schemaPath.slice(0, -1);
|
|
1570
|
+
if (configPath.startsWith(prefix) && configPath.length > prefix.length) return true;
|
|
1571
|
+
}
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
function readPath(root, dotPath) {
|
|
1575
|
+
const normalized = normalizeDotPath(dotPath);
|
|
1576
|
+
if (!normalized) return {
|
|
1577
|
+
present: true,
|
|
1578
|
+
value: root
|
|
1579
|
+
};
|
|
1580
|
+
let current = root;
|
|
1581
|
+
for (const segment of normalized.split(".")) {
|
|
1582
|
+
if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, segment)) return {
|
|
1583
|
+
present: false,
|
|
1584
|
+
value: void 0
|
|
1585
|
+
};
|
|
1586
|
+
current = current[segment];
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
present: true,
|
|
1590
|
+
value: current
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
function stableJsonValue(value) {
|
|
1594
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
1595
|
+
if (!isRecord(value)) return value;
|
|
1596
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableJsonValue(value[key])]));
|
|
1597
|
+
}
|
|
1598
|
+
function displayValue(value, present = true) {
|
|
1599
|
+
if (!present || value === void 0) return "<missing>";
|
|
1600
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1601
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
1602
|
+
try {
|
|
1603
|
+
return JSON.stringify(stableJsonValue(value));
|
|
1604
|
+
} catch {
|
|
1605
|
+
return "<unserializable>";
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function valuesMatch(actual, expected) {
|
|
1609
|
+
if (!actual.present) return false;
|
|
1610
|
+
if (Object.is(actual.value, expected)) return true;
|
|
1611
|
+
if ((Array.isArray(actual.value) || isRecord(actual.value)) && (Array.isArray(expected) || isRecord(expected))) return displayValue(actual.value) === displayValue(expected);
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
function mismatchIssue(code, path, expected, actual, label) {
|
|
1615
|
+
const expectedDisplay = displayValue(expected);
|
|
1616
|
+
const actualDisplay = displayValue(actual.value, actual.present);
|
|
1617
|
+
return {
|
|
1618
|
+
code,
|
|
1619
|
+
path,
|
|
1620
|
+
expected: expectedDisplay,
|
|
1621
|
+
actual: actualDisplay,
|
|
1622
|
+
message: `${label} is ${actualDisplay}; expected ${expectedDisplay}.`
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function compareExpectedValue(issues, discovery, code, path, expected, label) {
|
|
1626
|
+
const actual = readPath(discovery, path);
|
|
1627
|
+
if (!valuesMatch(actual, expected)) issues.push(mismatchIssue(code, path, expected, actual, label));
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Compare independently resolved docs values with the public agent discovery and
|
|
1631
|
+
* config-schema surfaces. The helper is intentionally pure so doctor, review,
|
|
1632
|
+
* and golden evaluations can use the same deterministic drift rules.
|
|
1633
|
+
*/
|
|
1634
|
+
function analyzeAgentSurfaceDrift(options) {
|
|
1635
|
+
const issues = [];
|
|
1636
|
+
const schemaPaths = collectSchemaPaths(options.schemaOptions);
|
|
1637
|
+
const configPaths = Array.from(new Set(options.configOptionPaths.map(normalizeDotPath).filter(Boolean))).sort();
|
|
1638
|
+
for (const configPath of configPaths) {
|
|
1639
|
+
if (schemaCoversConfigPath(schemaPaths, configPath)) continue;
|
|
1640
|
+
issues.push({
|
|
1641
|
+
code: "config-schema-omission",
|
|
1642
|
+
path: configPath,
|
|
1643
|
+
expected: "documented schema option",
|
|
1644
|
+
actual: "<missing>",
|
|
1645
|
+
message: `Config option ${JSON.stringify(configPath)} is present but missing from the published config schema.`
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
const canonicalFields = Array.from(new Set(options.agentContractFields.map((field) => field.trim()).filter(Boolean))).sort();
|
|
1649
|
+
const discoveredFieldsValue = readPath(options.discovery, "agentContract.fields");
|
|
1650
|
+
const discoveredFields = isRecord(discoveredFieldsValue.value) ? Object.keys(discoveredFieldsValue.value).sort() : [];
|
|
1651
|
+
const canonicalFieldSet = new Set(canonicalFields);
|
|
1652
|
+
const discoveredFieldSet = new Set(discoveredFields);
|
|
1653
|
+
for (const field of canonicalFields) {
|
|
1654
|
+
if (discoveredFieldSet.has(field)) continue;
|
|
1655
|
+
const issuePath = `agentContract.fields.${field}`;
|
|
1656
|
+
issues.push({
|
|
1657
|
+
code: "agent-contract-field-missing",
|
|
1658
|
+
path: issuePath,
|
|
1659
|
+
expected: "canonical field",
|
|
1660
|
+
actual: "<missing>",
|
|
1661
|
+
message: `Discovery agent contract is missing canonical field ${JSON.stringify(field)}.`
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
for (const field of discoveredFields) {
|
|
1665
|
+
if (canonicalFieldSet.has(field)) continue;
|
|
1666
|
+
const issuePath = `agentContract.fields.${field}`;
|
|
1667
|
+
issues.push({
|
|
1668
|
+
code: "agent-contract-field-unexpected",
|
|
1669
|
+
path: issuePath,
|
|
1670
|
+
expected: "<absent>",
|
|
1671
|
+
actual: "declared field",
|
|
1672
|
+
message: `Discovery agent contract advertises non-canonical field ${JSON.stringify(field)}.`
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
compareExpectedValue(issues, options.discovery, "entry-mismatch", "site.entry", options.expected.entry, "Discovery site.entry");
|
|
1676
|
+
compareExpectedValue(issues, options.discovery, "search-enabled-mismatch", "search.enabled", options.expected.search.enabled, "Discovery search.enabled");
|
|
1677
|
+
compareExpectedValue(issues, options.discovery, "search-capability-mismatch", "capabilities.search", options.expected.search.enabled, "Discovery capabilities.search");
|
|
1678
|
+
compareExpectedValue(issues, options.discovery, "search-route-mismatch", "search.endpoint", options.expected.search.endpoint, "Discovery search.endpoint");
|
|
1679
|
+
const expectedAgentSearchEndpoint = options.expected.search.endpoint === null ? null : `${options.expected.search.endpoint}${options.expected.search.endpoint.includes("?") ? "&" : "?"}audience=agent`;
|
|
1680
|
+
compareExpectedValue(issues, options.discovery, "search-audience-mismatch", "search.agentEndpoint", expectedAgentSearchEndpoint, "Discovery search.agentEndpoint");
|
|
1681
|
+
compareExpectedValue(issues, options.discovery, "search-audience-mismatch", "search.audienceParam", "audience", "Discovery search.audienceParam");
|
|
1682
|
+
compareExpectedValue(issues, options.discovery, "search-audience-mismatch", "search.defaultAudience", "human", "Discovery search.defaultAudience");
|
|
1683
|
+
compareExpectedValue(issues, options.discovery, "search-audience-mismatch", "search.supportedAudiences", ["human", "agent"], "Discovery search.supportedAudiences");
|
|
1684
|
+
compareExpectedValue(issues, options.discovery, "mcp-enabled-mismatch", "mcp.enabled", options.expected.mcp.enabled, "Discovery mcp.enabled");
|
|
1685
|
+
compareExpectedValue(issues, options.discovery, "mcp-capability-mismatch", "capabilities.mcp", options.expected.mcp.enabled, "Discovery capabilities.mcp");
|
|
1686
|
+
compareExpectedValue(issues, options.discovery, "mcp-route-mismatch", "mcp.endpoint", options.expected.mcp.endpoint, "Discovery mcp.endpoint");
|
|
1687
|
+
if (options.expected.mcp.protectedResource !== void 0) {
|
|
1688
|
+
const expectedProtectedResource = options.expected.mcp.protectedResource;
|
|
1689
|
+
const discoveredProtectedResource = readPath(options.discovery, "mcp.protectedResource");
|
|
1690
|
+
if (expectedProtectedResource === null) {
|
|
1691
|
+
if (discoveredProtectedResource.present) issues.push(mismatchIssue("mcp-protected-resource-mismatch", "mcp.protectedResource", void 0, discoveredProtectedResource, "Discovery mcp.protectedResource"));
|
|
1692
|
+
} else for (const field of [
|
|
1693
|
+
"metadataEndpoints",
|
|
1694
|
+
"authorizationServers",
|
|
1695
|
+
"scopesSupported",
|
|
1696
|
+
"requiredScopes"
|
|
1697
|
+
]) compareExpectedValue(issues, options.discovery, "mcp-protected-resource-mismatch", `mcp.protectedResource.${field}`, expectedProtectedResource[field], `Discovery mcp.protectedResource.${field}`);
|
|
1698
|
+
}
|
|
1699
|
+
const expectedToolNames = Object.keys(options.expected.mcp.tools).sort();
|
|
1700
|
+
const expectedToolSet = new Set(expectedToolNames);
|
|
1701
|
+
const discoveredToolsValue = readPath(options.discovery, "mcp.tools");
|
|
1702
|
+
const discoveredTools = isRecord(discoveredToolsValue.value) ? discoveredToolsValue.value : {};
|
|
1703
|
+
for (const toolName of expectedToolNames) {
|
|
1704
|
+
const expected = options.expected.mcp.tools[toolName];
|
|
1705
|
+
const path = `mcp.tools.${toolName}`;
|
|
1706
|
+
const actual = Object.prototype.hasOwnProperty.call(discoveredTools, toolName) ? {
|
|
1707
|
+
present: true,
|
|
1708
|
+
value: discoveredTools[toolName]
|
|
1709
|
+
} : {
|
|
1710
|
+
present: false,
|
|
1711
|
+
value: void 0
|
|
1712
|
+
};
|
|
1713
|
+
if (!valuesMatch(actual, expected)) issues.push(mismatchIssue("mcp-tool-mismatch", path, expected, actual, `Discovery ${path}`));
|
|
1714
|
+
}
|
|
1715
|
+
for (const toolName of Object.keys(discoveredTools).sort()) {
|
|
1716
|
+
if (expectedToolSet.has(toolName)) continue;
|
|
1717
|
+
const path = `mcp.tools.${toolName}`;
|
|
1718
|
+
issues.push({
|
|
1719
|
+
code: "mcp-tool-unexpected",
|
|
1720
|
+
path,
|
|
1721
|
+
expected: "<absent>",
|
|
1722
|
+
actual: displayValue(discoveredTools[toolName]),
|
|
1723
|
+
message: `Discovery MCP tools advertise unexpected tool flag ${JSON.stringify(toolName)}.`
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
for (const routePath of Object.keys(options.expected.routes).sort()) {
|
|
1727
|
+
const expected = options.expected.routes[routePath];
|
|
1728
|
+
compareExpectedValue(issues, options.discovery, "route-mismatch", routePath, expected, `Discovery ${routePath}`);
|
|
1729
|
+
}
|
|
1730
|
+
return issues.sort((left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code) || left.message.localeCompare(right.message));
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
//#endregion
|
|
1734
|
+
//#region src/cli/golden-evaluations.ts
|
|
1735
|
+
function isPlainRecord(value) {
|
|
1736
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1737
|
+
}
|
|
1738
|
+
/** Normalize the runtime docs.config evaluation shape shared by doctor and review. */
|
|
1739
|
+
function resolveGoldenEvaluationInput(evaluationInput) {
|
|
1740
|
+
if (evaluationInput === void 0 || typeof evaluationInput === "boolean") return {
|
|
1741
|
+
tasks: void 0,
|
|
1742
|
+
options: {}
|
|
1743
|
+
};
|
|
1744
|
+
if (!isPlainRecord(evaluationInput)) return {
|
|
1745
|
+
tasks: { evaluationConfig: evaluationInput },
|
|
1746
|
+
options: {}
|
|
1747
|
+
};
|
|
1748
|
+
const options = {
|
|
1749
|
+
surface: evaluationInput.surface,
|
|
1750
|
+
allowNetwork: evaluationInput.allowNetwork,
|
|
1751
|
+
searchTimeoutMs: evaluationInput.searchTimeoutMs,
|
|
1752
|
+
answer: evaluationInput.answer
|
|
1753
|
+
};
|
|
1754
|
+
if (evaluationInput.enabled === false) return {
|
|
1755
|
+
tasks: void 0,
|
|
1756
|
+
options
|
|
1757
|
+
};
|
|
1758
|
+
if ("enabled" in evaluationInput && typeof evaluationInput.enabled !== "boolean") return {
|
|
1759
|
+
tasks: evaluationInput,
|
|
1760
|
+
options
|
|
1761
|
+
};
|
|
1762
|
+
if (!("tasks" in evaluationInput)) return {
|
|
1763
|
+
tasks: void 0,
|
|
1764
|
+
options
|
|
1765
|
+
};
|
|
1766
|
+
const runtimeTasks = evaluationInput.tasks;
|
|
1767
|
+
if (!Array.isArray(runtimeTasks)) return {
|
|
1768
|
+
tasks: runtimeTasks,
|
|
1769
|
+
options
|
|
1770
|
+
};
|
|
1771
|
+
return {
|
|
1772
|
+
tasks: runtimeTasks.map((task) => {
|
|
1773
|
+
if (!isPlainRecord(task)) return task;
|
|
1774
|
+
return {
|
|
1775
|
+
...task,
|
|
1776
|
+
tokenBudget: task.tokenBudget ?? evaluationInput.tokenBudget,
|
|
1777
|
+
topK: task.topK ?? evaluationInput.topK
|
|
1778
|
+
};
|
|
1779
|
+
}),
|
|
1780
|
+
options
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
//#endregion
|
|
1785
|
+
export { extractAgentBlocks as a, createAgentUsefulnessPagesFromMcp as i, analyzeAgentSurfaceDrift as n, analyzeAgentUsefulness as r, resolveGoldenEvaluationInput as t };
|