@farming-labs/docs 0.2.60 → 0.2.62

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