@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.
Files changed (58) hide show
  1. package/dist/agent-CQTH7NFu.mjs +624 -0
  2. package/dist/agent-DKKptIgy.mjs +4365 -0
  3. package/dist/agent-evals-B7MIxuEX.mjs +2144 -0
  4. package/dist/agent-export-CBgWgPvH.mjs +910 -0
  5. package/dist/agent-scope-C_U--OZ7.mjs +283 -0
  6. package/dist/agent-skills-bundle.d.mts +13 -0
  7. package/dist/agent-skills-bundle.mjs +12 -0
  8. package/dist/agent-skills-server-CPja6Syt.d.mts +14 -0
  9. package/dist/agent-skills-server-DraIb6FV.mjs +415 -0
  10. package/dist/agent-skills-vite.d.mts +31 -0
  11. package/dist/agent-skills-vite.mjs +70 -0
  12. package/dist/agents-XWZBub6f.mjs +221 -0
  13. package/dist/analytics-Bx44lg6d.mjs +177 -0
  14. package/dist/cli/index.d.mts +15 -0
  15. package/dist/cli/index.mjs +452 -0
  16. package/dist/client/react.d.mts +45 -0
  17. package/dist/client/react.mjs +223 -0
  18. package/dist/cloud-analytics-CSyFE6SS.mjs +132 -0
  19. package/dist/cloud-ask-ai-sbpjOR2K.mjs +382 -0
  20. package/dist/cloud-ask-ai-zpwkdwnF.d.mts +23 -0
  21. package/dist/cloud-pdNC-tyj.mjs +1615 -0
  22. package/dist/code-blocks-DnNVNK2M.mjs +871 -0
  23. package/dist/codeblocks-CFuurVIH.mjs +250 -0
  24. package/dist/config-Wcdj-D0a.mjs +369 -0
  25. package/dist/dev-Cmy6DtdF.mjs +1333 -0
  26. package/dist/docs-cloud-server.d.mts +70 -0
  27. package/dist/docs-cloud-server.mjs +310 -0
  28. package/dist/doctor-DtGYZ41i.mjs +2036 -0
  29. package/dist/downgrade-w7e6Se0L.mjs +184 -0
  30. package/dist/errors-DbOhkE1h.mjs +20 -0
  31. package/dist/golden-evaluations-Dj-9Eo3v.mjs +1785 -0
  32. package/dist/i18n-CCaFUnAN.mjs +40 -0
  33. package/dist/index.d.mts +1150 -0
  34. package/dist/index.mjs +10 -0
  35. package/dist/init-CQY0Woe3.mjs +1264 -0
  36. package/dist/mcp-B9dcsivk.mjs +156 -0
  37. package/dist/mcp.d.mts +298 -0
  38. package/dist/mcp.mjs +4430 -0
  39. package/dist/metadata-DWExHQnx.mjs +237 -0
  40. package/dist/package-version-n5AFur8a.mjs +128 -0
  41. package/dist/reading-time-CYZ5VvKU.mjs +742 -0
  42. package/dist/review-CLoHTywU.mjs +673 -0
  43. package/dist/robots-BIpC4j4P.mjs +201 -0
  44. package/dist/robots-CUTahhoY.mjs +179 -0
  45. package/dist/search-B6V6qtiI.mjs +1826 -0
  46. package/dist/search-CaSyi6H6.d.mts +279 -0
  47. package/dist/search-DSjCeOk7.mjs +104 -0
  48. package/dist/server.d.mts +343 -0
  49. package/dist/server.mjs +14 -0
  50. package/dist/sitemap-Cykpe3Tz.mjs +249 -0
  51. package/dist/sitemap-server-C_6Wes83.mjs +1137 -0
  52. package/dist/standards-discovery-C4HUqMd2.d.mts +227 -0
  53. package/dist/standards-discovery-jkykaXq1.mjs +519 -0
  54. package/dist/templates-Bq_P7ctv.mjs +2465 -0
  55. package/dist/types-lMBIdZg0.d.mts +3315 -0
  56. package/dist/upgrade-oz-GChgt.mjs +56 -0
  57. package/dist/utils-DpiIioYb.mjs +225 -0
  58. package/package.json +1 -1
@@ -0,0 +1,673 @@
1
+ import { Ht as PAGE_AGENT_CONTRACT_FIELDS, Jt as normalizePageAgentFrontmatter, Kt as getPageAgentFrontmatterIssues, T as DOCS_CONFIG_MAP_TOP_LEVEL_KEYS, bt as findDocsAudienceMdxIssues, in as getDocsMcpProtectedResourceMetadataRoutes, j as buildDocsConfigMap, k as buildDocsAgentDiscoverySpec, qt as hasStructuredPageAgentContract } from "./agent-DKKptIgy.mjs";
2
+ import { c as DEFAULT_AGENT_SKILLS_INDEX_ROUTE, g as DEFAULT_API_CATALOG_ROUTE, h as DEFAULT_API_CATALOG_FORMAT, k as resolveDocsDiscoveryApiRoute, l as DEFAULT_AGENT_SKILLS_ROUTE_PATTERN, p as DEFAULT_AGENT_SKILL_FORMAT, s as DEFAULT_AGENT_SKILLS_INDEX_FORMAT } from "./standards-discovery-jkykaXq1.mjs";
3
+ import { d as resolveAskAISearchRequestConfig } from "./search-B6V6qtiI.mjs";
4
+ import { a as resolveDocsMetadataBaseUrl } from "./metadata-DWExHQnx.mjs";
5
+ import { c as resolveDocsReviewConfig, o as ensureDocsReviewWorkflow, s as readDocsReviewConfigFromSource, t as runDocsGoldenTasks } from "./agent-evals-B7MIxuEX.mjs";
6
+ import { createFilesystemDocsMcpSource, getDocsConfigSchema, resolveDocsMcpConfig } from "./mcp.mjs";
7
+ import "./code-blocks-DnNVNK2M.mjs";
8
+ import { _ as resolveDocsContentDir, g as resolveDocsConfigPath, h as readTopLevelStringProperty, s as loadDocsConfigModuleResultWithProjectEnv } from "./config-Wcdj-D0a.mjs";
9
+ import { t as detectFramework } from "./utils-DpiIioYb.mjs";
10
+ import { a as extractAgentBlocks, i as createAgentUsefulnessPagesFromMcp, n as analyzeAgentSurfaceDrift, r as analyzeAgentUsefulness, t as resolveGoldenEvaluationInput } from "./golden-evaluations-Dj-9Eo3v.mjs";
11
+ import matter from "gray-matter";
12
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
13
+ import path from "node:path";
14
+ import { execFileSync } from "node:child_process";
15
+ import pc from "picocolors";
16
+
17
+ //#region src/cli/review.ts
18
+ const DOCS_FILE_PATTERN = /\.(?:md|mdx|svx)$/;
19
+ const IGNORED_DIRS = new Set([
20
+ ".git",
21
+ ".next",
22
+ ".nuxt",
23
+ ".output",
24
+ ".svelte-kit",
25
+ "coverage",
26
+ "dist",
27
+ "node_modules",
28
+ "out"
29
+ ]);
30
+ function parseReviewArgs(argv) {
31
+ const parsed = {};
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const arg = argv[index];
34
+ if (arg === "setup") {
35
+ parsed.setup = true;
36
+ continue;
37
+ }
38
+ if (arg === "--help" || arg === "-h") {
39
+ parsed.help = true;
40
+ continue;
41
+ }
42
+ if (arg === "--ci") {
43
+ parsed.ci = true;
44
+ continue;
45
+ }
46
+ if (arg === "--json") {
47
+ parsed.json = true;
48
+ continue;
49
+ }
50
+ if (arg === "--config" || arg === "--base" || arg === "--head" || arg === "--mode") {
51
+ const value = argv[index + 1];
52
+ if (!value || value.startsWith("--")) throw new Error(`Missing value for ${arg}.`);
53
+ if (arg === "--config") parsed.configPath = value;
54
+ if (arg === "--base") parsed.base = value;
55
+ if (arg === "--head") parsed.head = value;
56
+ if (arg === "--mode") parsed.mode = parseReviewCiMode(value);
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (arg === "--score-threshold") {
61
+ const value = argv[index + 1];
62
+ if (!value || value.startsWith("--")) throw new Error("Missing value for --score-threshold.");
63
+ parsed.scoreThreshold = Number.parseInt(value, 10);
64
+ index += 1;
65
+ continue;
66
+ }
67
+ if (arg.startsWith("--config=")) {
68
+ parsed.configPath = readInlineFlag(arg, "--config=");
69
+ continue;
70
+ }
71
+ if (arg.startsWith("--base=")) {
72
+ parsed.base = readInlineFlag(arg, "--base=");
73
+ continue;
74
+ }
75
+ if (arg.startsWith("--head=")) {
76
+ parsed.head = readInlineFlag(arg, "--head=");
77
+ continue;
78
+ }
79
+ if (arg.startsWith("--mode=")) {
80
+ parsed.mode = parseReviewCiMode(readInlineFlag(arg, "--mode="));
81
+ continue;
82
+ }
83
+ if (arg.startsWith("--score-threshold=")) {
84
+ parsed.scoreThreshold = Number.parseInt(readInlineFlag(arg, "--score-threshold="), 10);
85
+ continue;
86
+ }
87
+ throw new Error(`Unknown review flag: ${arg}.`);
88
+ }
89
+ return parsed;
90
+ }
91
+ async function runReview(options = {}) {
92
+ const rootDir = process.cwd();
93
+ const configPath = resolveDocsConfigPath(rootDir, options.configPath);
94
+ const configContent = readFileSync(configPath, "utf-8");
95
+ const configLoad = await loadDocsConfigModuleResultWithProjectEnv(rootDir, options.configPath);
96
+ const config = configLoad.status === "evaluated" ? configLoad.config : void 0;
97
+ const review = withReviewOptionOverrides(resolveDocsReviewConfig(config?.review ?? readDocsReviewConfigFromSource(configContent)), options.mode, options.scoreThreshold);
98
+ if (options.setup) {
99
+ const result = ensureDocsReviewWorkflow({
100
+ rootDir,
101
+ config,
102
+ configPath: path.relative(rootDir, configPath),
103
+ configContent,
104
+ log: (message) => console.log(pc.green(message))
105
+ });
106
+ if (result.status === "exists") console.log(pc.dim(`${result.relativePath} already exists.`));
107
+ if (result.status === "disabled") console.log(pc.yellow("Docs Review CI is disabled in docs.config."));
108
+ return;
109
+ }
110
+ if (!review.enabled) {
111
+ const report = {
112
+ status: "disabled",
113
+ score: null,
114
+ threshold: review.score.threshold,
115
+ mode: options.ci ? review.ci.mode : "local",
116
+ reviewedFiles: [],
117
+ changedFiles: [],
118
+ findings: []
119
+ };
120
+ if (options.json) console.log(JSON.stringify(report, null, 2));
121
+ else console.log(pc.yellow("Docs Review is disabled in docs.config."));
122
+ return report;
123
+ }
124
+ const entry = config?.entry ?? readTopLevelStringProperty(configContent, "entry") ?? "docs";
125
+ const contentDir = config?.contentDir ?? resolveDocsContentDir(rootDir, configContent, entry);
126
+ const pages = scanDocsPages(rootDir, contentDir, entry);
127
+ const changedFiles = getChangedFiles(rootDir, options);
128
+ const relevantFiles = selectReviewFiles({
129
+ changedFiles,
130
+ pages,
131
+ configPath: path.relative(rootDir, configPath),
132
+ rootDir,
133
+ contentDir
134
+ });
135
+ const mcpSource = createFilesystemDocsMcpSource({
136
+ rootDir,
137
+ entry,
138
+ contentDir,
139
+ siteTitle: typeof config?.nav?.title === "string" ? config.nav.title : "Documentation"
140
+ });
141
+ const corpusPages = await Promise.resolve(mcpSource.getPages());
142
+ const usefulness = analyzeAgentUsefulness({
143
+ rootDir,
144
+ pages: createAgentUsefulnessPagesFromMcp(rootDir, corpusPages),
145
+ projectFramework: detectFramework(rootDir) ?? void 0
146
+ });
147
+ const findings = collectReviewFindings({
148
+ rootDir,
149
+ entry,
150
+ pages,
151
+ files: relevantFiles,
152
+ review
153
+ });
154
+ const relevantSet = new Set(relevantFiles);
155
+ for (const issue of usefulness.findings) {
156
+ if (!relevantSet.has(issue.file)) continue;
157
+ pushFinding(findings, review, {
158
+ rule: issue.category === "command" ? "commandHealth" : issue.category === "related" ? "relatedCoverage" : "agentContext",
159
+ code: issue.code,
160
+ severity: issue.severity === "warning" ? "warn" : issue.severity === "error" ? "error" : "suggestion",
161
+ file: issue.file,
162
+ line: issue.line,
163
+ message: issue.message
164
+ });
165
+ }
166
+ const evaluationInput = config?.agent?.evaluations;
167
+ const evaluation = resolveGoldenEvaluationInput(evaluationInput);
168
+ const evaluationBaseUrl = config ? resolveDocsMetadataBaseUrl(config) : void 0;
169
+ const evaluationSiteTitle = typeof config?.nav?.title === "string" ? config.nav.title : void 0;
170
+ const evaluationMcp = resolveDocsMcpConfig(config?.mcp, { defaultName: evaluationSiteTitle });
171
+ const askAISearch = resolveAskAISearchRequestConfig({
172
+ search: config?.search,
173
+ useMcp: config?.ai?.useMcp,
174
+ mcpEndpoint: evaluationMcp.route,
175
+ mcpEnabled: evaluationMcp.enabled,
176
+ mcpSearchEnabled: evaluationMcp.tools.searchDocs,
177
+ requestUrl: evaluationBaseUrl
178
+ });
179
+ const evaluations = await runDocsGoldenTasks(corpusPages, evaluation.tasks, {
180
+ ...evaluation.options,
181
+ search: config?.search,
182
+ askAISearch,
183
+ siteTitle: evaluationSiteTitle,
184
+ baseUrl: evaluationBaseUrl,
185
+ rootDir,
186
+ codeBlocksValidate: config?.codeBlocks?.validate
187
+ });
188
+ if (relevantFiles.length > 0 && evaluations.status === "unmeasured") pushFinding(findings, review, {
189
+ rule: "goldenTasks",
190
+ code: "golden-tasks-unmeasured",
191
+ severity: "warn",
192
+ file: toPosixPath(path.relative(rootDir, configPath)),
193
+ message: evaluationInput === false || typeof evaluationInput === "object" && evaluationInput.enabled === false ? "Golden agent tasks are disabled, so retrieval usefulness is unmeasured." : "No golden agent tasks are configured; retrieval, citations, version selection, executable examples, and token usage are unmeasured."
194
+ });
195
+ for (const task of evaluations.tasks.filter((task) => !task.passed)) pushFinding(findings, review, {
196
+ rule: "goldenTasks",
197
+ code: "golden-task-failed",
198
+ severity: "warn",
199
+ file: toPosixPath(path.relative(rootDir, configPath)),
200
+ message: `Golden task ${JSON.stringify(task.id)} scored ${task.score}/100: ${task.issues.join(" ")}`
201
+ });
202
+ if (configLoad.status === "static-fallback") pushFinding(findings, review, {
203
+ rule: "configConfidence",
204
+ code: "config-static-fallback",
205
+ severity: "warn",
206
+ file: toPosixPath(path.relative(rootDir, configPath)),
207
+ message: `docs.config could not be evaluated; review used partial static parsing. ${configLoad.error}`
208
+ });
209
+ else {
210
+ const evaluatedConfig = configLoad.config;
211
+ const searchEnabled = evaluatedConfig.search !== false && !(typeof evaluatedConfig.search === "object" && evaluatedConfig.search?.enabled === false);
212
+ const mcp = resolveDocsMcpConfig(evaluatedConfig.mcp, { defaultName: typeof evaluatedConfig.nav?.title === "string" ? evaluatedConfig.nav.title : void 0 });
213
+ const apiRoute = resolveDocsDiscoveryApiRoute(evaluatedConfig.cloud?.apiRoute);
214
+ const discovery = buildDocsAgentDiscoverySpec({
215
+ origin: "http://localhost",
216
+ entry,
217
+ apiRoute,
218
+ search: evaluatedConfig.search,
219
+ mcp
220
+ });
221
+ const configuredAgentReviewPaths = Object.values(buildDocsConfigMap(evaluatedConfig).pointers).map((pointer) => pointer.path).filter((optionPath) => optionPath === "agent" || optionPath.startsWith("agent.") || optionPath === "mcp" || optionPath.startsWith("mcp.") || optionPath === "review" || optionPath.startsWith("review."));
222
+ const expectedMcpProtectedResource = mcp.enabled && mcp.security?.authenticate && mcp.security.protectedResource ? {
223
+ metadataEndpoints: [...getDocsMcpProtectedResourceMetadataRoutes(mcp.route)],
224
+ authorizationServers: mcp.security.protectedResource.authorizationServers,
225
+ scopesSupported: mcp.security.protectedResource.scopesSupported,
226
+ requiredScopes: mcp.security.protectedResource.requiredScopes
227
+ } : null;
228
+ const drift = analyzeAgentSurfaceDrift({
229
+ configOptionPaths: [...new Set([...DOCS_CONFIG_MAP_TOP_LEVEL_KEYS, ...configuredAgentReviewPaths])],
230
+ schemaOptions: getDocsConfigSchema().options,
231
+ agentContractFields: PAGE_AGENT_CONTRACT_FIELDS,
232
+ discovery,
233
+ expected: {
234
+ entry,
235
+ search: {
236
+ enabled: searchEnabled,
237
+ endpoint: `${apiRoute}?query={query}`
238
+ },
239
+ mcp: {
240
+ enabled: mcp.enabled,
241
+ endpoint: mcp.route,
242
+ tools: mcp.tools,
243
+ protectedResource: expectedMcpProtectedResource
244
+ },
245
+ routes: {
246
+ "api.docs": apiRoute,
247
+ "api.config": `${apiRoute}?format=config`,
248
+ "api.apiCatalog": DEFAULT_API_CATALOG_ROUTE,
249
+ "api.apiCatalogQuery": `${apiRoute}?format=${DEFAULT_API_CATALOG_FORMAT}`,
250
+ "api.agentSkillsIndex": DEFAULT_AGENT_SKILLS_INDEX_ROUTE,
251
+ "apiCatalog.route": DEFAULT_API_CATALOG_ROUTE,
252
+ "apiCatalog.api": `${apiRoute}?format=${DEFAULT_API_CATALOG_FORMAT}`,
253
+ "config.endpoint": `${apiRoute}?format=config`,
254
+ "skills.discovery.index": DEFAULT_AGENT_SKILLS_INDEX_ROUTE,
255
+ "skills.discovery.artifact": DEFAULT_AGENT_SKILLS_ROUTE_PATTERN,
256
+ "skills.discovery.apiIndex": `${apiRoute}?format=${DEFAULT_AGENT_SKILLS_INDEX_FORMAT}`,
257
+ "skills.discovery.apiArtifact": `${apiRoute}?format=${DEFAULT_AGENT_SKILL_FORMAT}&name={name}`
258
+ }
259
+ }
260
+ });
261
+ for (const issue of drift) pushFinding(findings, review, {
262
+ rule: "agentSurfaceDrift",
263
+ code: issue.code,
264
+ severity: "error",
265
+ file: toPosixPath(path.relative(rootDir, configPath)),
266
+ message: issue.message
267
+ });
268
+ }
269
+ const score = calculateReviewScore(findings, review);
270
+ const mode = options.ci ? review.ci.mode : "local";
271
+ const report = {
272
+ status: "measured",
273
+ score,
274
+ threshold: review.score.threshold,
275
+ mode,
276
+ reviewedFiles: relevantFiles,
277
+ changedFiles,
278
+ findings,
279
+ usefulness: usefulness.metrics,
280
+ evaluations
281
+ };
282
+ if (options.json) console.log(JSON.stringify(report, null, 2));
283
+ else printReviewReport(report);
284
+ if (options.ci && review.ci.annotations) emitGitHubAnnotations(report.findings);
285
+ if (options.ci && review.ci.mode === "block" && (score < review.score.threshold || findings.some((finding) => finding.severity === "error"))) process.exitCode = 1;
286
+ return report;
287
+ }
288
+ function printReviewHelp() {
289
+ console.log(`
290
+ ${pc.bold("docs review")} — Review changed docs content for CI and agent-readiness.
291
+
292
+ ${pc.dim("Usage:")}
293
+ pnpm exec docs ${pc.cyan("review")} ${pc.dim("[--ci]")}
294
+ pnpm exec docs ${pc.cyan("review setup")}
295
+
296
+ ${pc.dim("Options:")}
297
+ ${pc.cyan("--ci")} Use docs.config review.ci behavior and GitHub annotations
298
+ ${pc.cyan("--json")} Print JSON report
299
+ ${pc.cyan("--config <path>")} Use a custom docs config path
300
+ ${pc.cyan("--base <ref> --head <ref>")} Review files changed between two git refs
301
+ ${pc.cyan("--mode <off|warn|block>")} Override review.ci.mode
302
+ ${pc.cyan("--score-threshold <0-100>")} Override review.score.threshold
303
+ `);
304
+ }
305
+ function withReviewOptionOverrides(review, mode, scoreThreshold) {
306
+ return {
307
+ ...review,
308
+ ci: mode ? {
309
+ ...review.ci,
310
+ mode,
311
+ enabled: mode !== "off"
312
+ } : review.ci,
313
+ score: typeof scoreThreshold === "number" && Number.isFinite(scoreThreshold) ? {
314
+ ...review.score,
315
+ threshold: Math.max(0, Math.min(100, Math.round(scoreThreshold)))
316
+ } : review.score
317
+ };
318
+ }
319
+ function collectReviewFindings(options) {
320
+ const findings = [];
321
+ const knownRoutes = /* @__PURE__ */ new Set();
322
+ const slugSources = /* @__PURE__ */ new Map();
323
+ for (const page of options.pages) {
324
+ knownRoutes.add(page.route);
325
+ knownRoutes.add(page.markdownRoute);
326
+ if (slugSources.has(page.route)) pushFinding(findings, options.review, {
327
+ rule: "duplicateSlugs",
328
+ severity: "error",
329
+ file: page.relativePath,
330
+ message: `Duplicate docs route ${page.route}; already used by ${slugSources.get(page.route)}.`
331
+ });
332
+ else slugSources.set(page.route, page.relativePath);
333
+ }
334
+ for (const file of options.files) {
335
+ if (!DOCS_FILE_PATTERN.test(file)) continue;
336
+ const absolutePath = path.join(options.rootDir, file);
337
+ if (!existsSync(absolutePath)) continue;
338
+ let source = "";
339
+ let parsed;
340
+ try {
341
+ source = readFileSync(absolutePath, "utf-8");
342
+ parsed = matter(source);
343
+ } catch (error) {
344
+ pushFinding(findings, options.review, {
345
+ rule: "invalidMdx",
346
+ severity: "error",
347
+ file,
348
+ message: `Could not read or parse this docs file: ${error instanceof Error ? error.message : String(error)}`
349
+ });
350
+ continue;
351
+ }
352
+ if (!file.endsWith("agent.md")) {
353
+ checkFrontmatter(findings, options.review, file, parsed.data);
354
+ checkAgentContext(findings, options.review, {
355
+ file,
356
+ source,
357
+ rootDir: options.rootDir,
358
+ agent: parsed.data.agent
359
+ });
360
+ }
361
+ checkBrokenLinks(findings, options.review, {
362
+ file,
363
+ source,
364
+ entry: options.entry,
365
+ knownRoutes
366
+ });
367
+ checkCodeFences(findings, options.review, {
368
+ file,
369
+ source
370
+ });
371
+ }
372
+ return findings;
373
+ }
374
+ function checkFrontmatter(findings, review, file, data) {
375
+ if (!data.title || typeof data.title !== "string") pushFinding(findings, review, {
376
+ rule: "frontmatter",
377
+ severity: "error",
378
+ file,
379
+ line: 1,
380
+ message: "Missing frontmatter title."
381
+ });
382
+ if (!data.description || typeof data.description !== "string") pushFinding(findings, review, {
383
+ rule: "frontmatter",
384
+ severity: "error",
385
+ file,
386
+ line: 1,
387
+ message: "Missing frontmatter description."
388
+ });
389
+ }
390
+ function checkBrokenLinks(findings, review, options) {
391
+ const linkPattern = /\[[^\]]+\]\(([^)]+)\)|href=["']([^"']+)["']/g;
392
+ let match;
393
+ while (match = linkPattern.exec(options.source)) {
394
+ if (match[1] && options.source[match.index - 1] === "!") continue;
395
+ const href = match[1] ?? match[2];
396
+ if (!href || !href.startsWith("/")) continue;
397
+ const normalized = normalizeInternalHref(href);
398
+ if (!normalized || !normalized.startsWith(`/${options.entry}`)) continue;
399
+ if (options.knownRoutes.has(normalized)) continue;
400
+ pushFinding(findings, review, {
401
+ rule: "brokenLinks",
402
+ severity: "error",
403
+ file: options.file,
404
+ line: lineForIndex(options.source, match.index),
405
+ message: `Internal docs link does not resolve: ${href}`
406
+ });
407
+ }
408
+ }
409
+ function checkCodeFences(findings, review, options) {
410
+ const fencePattern = /^```([^\n`]*)\n([\s\S]*?)^```/gm;
411
+ let match;
412
+ while (match = fencePattern.exec(options.source)) {
413
+ const info = match[1]?.trim() ?? "";
414
+ const code = match[2] ?? "";
415
+ const language = info.split(/\s+/)[0] ?? "";
416
+ if (!language || !isImplementationLanguage(language)) continue;
417
+ const hasTitle = /\btitle=/.test(info);
418
+ const hasPackageManager = /\bpackageManager=/.test(info);
419
+ const isRunnable = /\brunnable\b/.test(info);
420
+ const isConfigExample = /\bdefineDocs\s*\(|\bwithDocs\s*\(|\bdocs\.config\b/.test(code);
421
+ if (!hasTitle && (isRunnable || isConfigExample)) pushFinding(findings, review, {
422
+ rule: "codeFenceMetadata",
423
+ severity: "warn",
424
+ file: options.file,
425
+ line: lineForIndex(options.source, match.index),
426
+ message: "Code block is missing title metadata, e.g. title=\"docs.config.ts\"."
427
+ });
428
+ if (isRunnable && !hasPackageManager) pushFinding(findings, review, {
429
+ rule: "runnableMetadata",
430
+ severity: "warn",
431
+ file: options.file,
432
+ line: lineForIndex(options.source, match.index),
433
+ message: "Runnable code block is missing packageManager metadata."
434
+ });
435
+ }
436
+ }
437
+ function checkAgentContext(findings, review, options) {
438
+ for (const issue of findDocsAudienceMdxIssues(options.source)) pushFinding(findings, review, {
439
+ rule: "agentContext",
440
+ code: `audience-${issue.code}`,
441
+ severity: issue.code === "dynamic-only" ? "error" : "warn",
442
+ file: options.file,
443
+ line: lineForIndex(options.source, issue.index),
444
+ message: issue.message
445
+ });
446
+ for (const issue of getPageAgentFrontmatterIssues(options.agent)) pushFinding(findings, review, {
447
+ rule: "agentContext",
448
+ severity: "warn",
449
+ file: options.file,
450
+ line: 1,
451
+ message: `Invalid ${issue.path}: ${issue.message}${/[.!?]$/.test(issue.message) ? "" : "."}`
452
+ });
453
+ const agent = normalizePageAgentFrontmatter(options.agent);
454
+ if (hasStructuredPageAgentContract(agent)) {
455
+ const missing = [!agent?.task ? "task" : void 0, !agent?.outcome ? "outcome" : void 0].filter(Boolean).join(" and ");
456
+ if (missing) pushFinding(findings, review, {
457
+ rule: "agentContext",
458
+ severity: "suggestion",
459
+ file: options.file,
460
+ line: 1,
461
+ message: `Structured agent contract is missing ${missing}.`
462
+ });
463
+ if (agent?.commands?.length && !agent.verification?.length) pushFinding(findings, review, {
464
+ rule: "agentContext",
465
+ severity: "suggestion",
466
+ file: options.file,
467
+ line: 1,
468
+ message: "Structured agent contract defines commands without verification steps."
469
+ });
470
+ if (agent?.sideEffects?.length && !agent.rollback?.length) pushFinding(findings, review, {
471
+ rule: "agentContext",
472
+ severity: "suggestion",
473
+ file: options.file,
474
+ line: 1,
475
+ message: "Structured agent contract defines side effects without rollback guidance."
476
+ });
477
+ if (agent?.task && agent.outcome) return;
478
+ }
479
+ if (extractAgentBlocks(options.source, { sourcePath: options.file }).length > 0) return;
480
+ if (existsSync(path.join(path.dirname(path.join(options.rootDir, options.file)), "agent.md"))) return;
481
+ if (!/\b(install|configure|setup|implement|defineDocs|docs\.config|MCP|agent)\b/i.test(options.source)) return;
482
+ pushFinding(findings, review, {
483
+ rule: "agentContext",
484
+ severity: "suggestion",
485
+ file: options.file,
486
+ line: 1,
487
+ message: "Implementation-heavy docs page could use an <Agent> block, <Audience only=\"agent\"> block, or sibling agent.md."
488
+ });
489
+ }
490
+ function pushFinding(findings, review, finding) {
491
+ const configured = review.rules[finding.rule];
492
+ if (configured === "off") return;
493
+ findings.push({
494
+ ...finding,
495
+ severity: configured === "error" || configured === "warn" ? configured : "suggestion"
496
+ });
497
+ }
498
+ function calculateReviewScore(findings, review) {
499
+ const penalty = findings.reduce((total, finding) => {
500
+ if (finding.severity === "error") return total + review.score.weights.error;
501
+ if (finding.severity === "warn") return total + review.score.weights.warn;
502
+ return total + review.score.weights.suggestion;
503
+ }, 0);
504
+ return Math.max(0, 100 - penalty);
505
+ }
506
+ function printReviewReport(report) {
507
+ const counts = countFindings(report.findings);
508
+ const modeLabel = report.mode === "local" ? "local" : report.mode;
509
+ console.log(pc.bold("Docs Review"));
510
+ console.log("");
511
+ console.log(`Score: ${scoreColor(report.score, report.threshold)} / 100`);
512
+ console.log(`Threshold: ${report.threshold}`);
513
+ console.log(`Mode: ${modeLabel}`);
514
+ console.log(`Changed files: ${report.changedFiles.length}`);
515
+ console.log(`Reviewed docs files: ${report.reviewedFiles.length}`);
516
+ console.log(`Findings: ${counts.error} error${counts.error === 1 ? "" : "s"}, ${counts.warn} warning${counts.warn === 1 ? "" : "s"}, ${counts.suggestion} suggestion${counts.suggestion === 1 ? "" : "s"}`);
517
+ if (report.usefulness) console.log(`Useful agent-only blocks: ${report.usefulness.agentBlocks.useful}/${report.usefulness.agentBlocks.total}; task-complete pages: ${report.usefulness.taskCompleteness.completePages}/${report.usefulness.actionablePages}`);
518
+ if (report.evaluations) console.log(`Golden tasks: ${report.evaluations.status === "unmeasured" ? "unmeasured" : `${report.evaluations.passedTaskCount}/${report.evaluations.taskCount} passed (${report.evaluations.score}/100)`}`);
519
+ if (report.reviewedFiles.length === 0 && report.findings.length === 0) {
520
+ console.log("");
521
+ console.log(pc.green("No docs changes detected. Skipping review."));
522
+ return;
523
+ }
524
+ if (report.reviewedFiles.length === 0) {
525
+ console.log("");
526
+ console.log(pc.yellow("No docs files changed; reporting project-wide findings."));
527
+ }
528
+ if (report.findings.length === 0) {
529
+ console.log("");
530
+ console.log(pc.green("No docs review findings."));
531
+ return;
532
+ }
533
+ console.log("");
534
+ for (const finding of report.findings) {
535
+ const label = finding.severity === "error" ? pc.red("ERROR") : finding.severity === "warn" ? pc.yellow("WARN") : pc.cyan("SUGGEST");
536
+ const location = `${finding.file}${finding.line ? `:${finding.line}` : ""}`;
537
+ console.log(`${label} ${pc.dim(location)} ${finding.message}`);
538
+ }
539
+ }
540
+ function emitGitHubAnnotations(findings) {
541
+ if (process.env.GITHUB_ACTIONS !== "true") return;
542
+ for (const finding of findings) {
543
+ const command = finding.severity === "error" ? "error" : finding.severity === "warn" ? "warning" : "notice";
544
+ const location = [`file=${escapeGitHubAnnotationValue(finding.file)}`, finding.line ? `line=${finding.line}` : void 0].filter(Boolean).join(",");
545
+ console.log(`::${command} ${location}::${escapeGitHubAnnotationValue(finding.message)}`);
546
+ }
547
+ }
548
+ function scoreColor(score, threshold) {
549
+ if (score < threshold) return pc.red(String(score));
550
+ if (score < Math.min(100, threshold + 10)) return pc.yellow(String(score));
551
+ return pc.green(String(score));
552
+ }
553
+ function countFindings(findings) {
554
+ return findings.reduce((counts, finding) => {
555
+ counts[finding.severity] += 1;
556
+ return counts;
557
+ }, {
558
+ error: 0,
559
+ warn: 0,
560
+ suggestion: 0
561
+ });
562
+ }
563
+ function selectReviewFiles(options) {
564
+ const pageFiles = new Set(options.pages.map((page) => page.relativePath));
565
+ const normalizedConfigPath = toPosixPath(options.configPath);
566
+ if (options.changedFiles.includes(normalizedConfigPath)) return Array.from(pageFiles).sort();
567
+ const selected = /* @__PURE__ */ new Set();
568
+ for (const changedFile of options.changedFiles.map(toPosixPath)) {
569
+ if (pageFiles.has(changedFile)) {
570
+ selected.add(changedFile);
571
+ continue;
572
+ }
573
+ if (!changedFile.endsWith("/agent.md") && changedFile !== "agent.md") continue;
574
+ const sibling = options.pages.find((page) => path.posix.dirname(page.relativePath) === path.posix.dirname(changedFile));
575
+ if (sibling) selected.add(sibling.relativePath);
576
+ }
577
+ return Array.from(selected).sort();
578
+ }
579
+ function getChangedFiles(rootDir, options) {
580
+ const ranges = [
581
+ options.base && options.head ? `${options.base}...${options.head}` : void 0,
582
+ process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}...HEAD` : void 0,
583
+ "HEAD~1...HEAD",
584
+ void 0
585
+ ].filter((range, index, allRanges) => range !== void 0 || index === allRanges.length - 1);
586
+ for (const range of ranges) try {
587
+ const args = [
588
+ "diff",
589
+ "--relative",
590
+ "--name-only",
591
+ "--diff-filter=ACMRTUXB"
592
+ ];
593
+ if (range) args.push(range);
594
+ const files = execFileSync("git", args, {
595
+ cwd: rootDir,
596
+ encoding: "utf-8"
597
+ }).split(/\r?\n/).map((file) => file.trim()).filter(Boolean).map(toPosixPath);
598
+ if (files.length > 0 || range === void 0) return files;
599
+ } catch {}
600
+ return [];
601
+ }
602
+ function scanDocsPages(rootDir, contentDir, entry) {
603
+ const contentRoot = path.isAbsolute(contentDir) ? contentDir : path.join(rootDir, contentDir);
604
+ if (!existsSync(contentRoot)) return [];
605
+ return listFiles(contentRoot).filter((file) => DOCS_FILE_PATTERN.test(file) && path.basename(file) !== "agent.md").map((absolutePath) => {
606
+ const relativeToContent = toPosixPath(path.relative(contentRoot, absolutePath));
607
+ const relativePath = toPosixPath(path.relative(rootDir, absolutePath));
608
+ const slug = docsSlugFromFile(relativeToContent);
609
+ const route = normalizeRoute(`/${entry}${slug ? `/${slug}` : ""}`);
610
+ return {
611
+ relativePath,
612
+ absolutePath,
613
+ route,
614
+ markdownRoute: `${route}.md`
615
+ };
616
+ });
617
+ }
618
+ function listFiles(dir) {
619
+ const files = [];
620
+ for (const name of readdirSync(dir)) {
621
+ if (IGNORED_DIRS.has(name)) continue;
622
+ const fullPath = path.join(dir, name);
623
+ let stat;
624
+ try {
625
+ stat = lstatSync(fullPath);
626
+ } catch {
627
+ continue;
628
+ }
629
+ if (stat.isDirectory()) files.push(...listFiles(fullPath));
630
+ else if (stat.isFile()) files.push(fullPath);
631
+ }
632
+ return files;
633
+ }
634
+ function docsSlugFromFile(relativePath) {
635
+ const withoutExt = relativePath.replace(/\.(?:md|mdx|svx)$/, "");
636
+ if (withoutExt === "page" || withoutExt === "+page" || withoutExt === "index") return "";
637
+ if (withoutExt.endsWith("/page") || withoutExt.endsWith("/+page") || withoutExt.endsWith("/index")) return withoutExt.replace(/\/(?:\+?page|index)$/, "");
638
+ return withoutExt;
639
+ }
640
+ function normalizeInternalHref(href) {
641
+ const [withoutHash] = href.split("#");
642
+ const [withoutQuery] = withoutHash.split("?");
643
+ if (!withoutQuery || withoutQuery === "/") return "/";
644
+ return normalizeRoute(withoutQuery.replace(/\.md$/, ""));
645
+ }
646
+ function normalizeRoute(route) {
647
+ const normalized = `/${route}`.replace(/\/+/g, "/");
648
+ return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
649
+ }
650
+ function isImplementationLanguage(language) {
651
+ return /^(?:bash|sh|shell|zsh|ts|tsx|js|jsx|json|mdx?)$/.test(language);
652
+ }
653
+ function lineForIndex(source, index) {
654
+ return source.slice(0, index).split(/\r?\n/).length;
655
+ }
656
+ function escapeGitHubAnnotationValue(value) {
657
+ return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
658
+ }
659
+ function parseReviewCiMode(value) {
660
+ if (value === "off" || value === "warn" || value === "block") return value;
661
+ throw new Error(`Invalid review mode: ${value}. Expected off, warn, or block.`);
662
+ }
663
+ function readInlineFlag(arg, prefix) {
664
+ const value = arg.slice(prefix.length);
665
+ if (!value) throw new Error(`Missing value for ${prefix.replace(/=$/, "")}.`);
666
+ return value;
667
+ }
668
+ function toPosixPath(value) {
669
+ return value.replaceAll("\\", "/");
670
+ }
671
+
672
+ //#endregion
673
+ export { parseReviewArgs, printReviewHelp, runReview };