@farming-labs/docs 0.2.61 → 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.
- package/package.json +1 -1
- package/dist/agent-DFHlw_JC.mjs +0 -624
- package/dist/agent-Fl0pjVNF.mjs +0 -4352
- package/dist/agent-evals-f4_OL10T.mjs +0 -2144
- package/dist/agent-export-D0zQhasD.mjs +0 -910
- package/dist/agent-scope-C_U--OZ7.mjs +0 -283
- package/dist/agent-skills-bundle.d.mts +0 -13
- package/dist/agent-skills-bundle.mjs +0 -12
- package/dist/agent-skills-server-CIeBszxp.mjs +0 -263
- package/dist/agent-skills-server-CKq3_jMj.d.mts +0 -12
- package/dist/agent-skills-vite.d.mts +0 -31
- package/dist/agent-skills-vite.mjs +0 -70
- package/dist/agents-ibnXrWyp.mjs +0 -221
- package/dist/analytics-Bx44lg6d.mjs +0 -177
- package/dist/cli/index.d.mts +0 -15
- package/dist/cli/index.mjs +0 -452
- package/dist/client/react.d.mts +0 -45
- package/dist/client/react.mjs +0 -223
- package/dist/cloud-analytics-CSyFE6SS.mjs +0 -132
- package/dist/cloud-ask-ai-D7ZgF47y.d.mts +0 -23
- package/dist/cloud-ask-ai-sbpjOR2K.mjs +0 -382
- package/dist/cloud-pdNC-tyj.mjs +0 -1615
- package/dist/code-blocks-DnNVNK2M.mjs +0 -871
- package/dist/codeblocks-CFuurVIH.mjs +0 -250
- package/dist/config-Wcdj-D0a.mjs +0 -369
- package/dist/dev-Cmy6DtdF.mjs +0 -1333
- package/dist/docs-cloud-server.d.mts +0 -70
- package/dist/docs-cloud-server.mjs +0 -310
- package/dist/doctor-CXostbsI.mjs +0 -2036
- package/dist/downgrade-w7e6Se0L.mjs +0 -184
- package/dist/errors-DbOhkE1h.mjs +0 -20
- package/dist/golden-evaluations-CBZ_JZjf.mjs +0 -1498
- package/dist/i18n-CCaFUnAN.mjs +0 -40
- package/dist/index.d.mts +0 -1141
- package/dist/index.mjs +0 -10
- package/dist/init-CQY0Woe3.mjs +0 -1264
- package/dist/mcp-DyPcoLwm.mjs +0 -156
- package/dist/mcp.d.mts +0 -298
- package/dist/mcp.mjs +0 -4426
- package/dist/metadata-Dv1ah0Aj.mjs +0 -237
- package/dist/package-version-n5AFur8a.mjs +0 -128
- package/dist/reading-time-C-SAhQT9.mjs +0 -742
- package/dist/review-D2UBrxFq.mjs +0 -673
- package/dist/robots-5Yqz9mz7.mjs +0 -179
- package/dist/robots-C08kDLsz.mjs +0 -201
- package/dist/search-9OnMGMvt.mjs +0 -1758
- package/dist/search-C6heDO8h.d.mts +0 -274
- package/dist/search-CNsRpz90.mjs +0 -104
- package/dist/server.d.mts +0 -342
- package/dist/server.mjs +0 -13
- package/dist/sitemap-BpYnSsfJ.mjs +0 -249
- package/dist/sitemap-server-D_0Kzanj.mjs +0 -1137
- package/dist/standards-discovery-C4HUqMd2.d.mts +0 -227
- package/dist/standards-discovery-C54V_aJH.mjs +0 -510
- package/dist/templates-Bq_P7ctv.mjs +0 -2465
- package/dist/types-EhFhYGfr.d.mts +0 -3305
- package/dist/upgrade-oz-GChgt.mjs +0 -56
- package/dist/utils-DpiIioYb.mjs +0 -225
package/dist/review-D2UBrxFq.mjs
DELETED
|
@@ -1,673 +0,0 @@
|
|
|
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-Fl0pjVNF.mjs";
|
|
2
|
-
import { O as resolveDocsDiscoveryApiRoute, c as DEFAULT_AGENT_SKILLS_INDEX_ROUTE, g as DEFAULT_API_CATALOG_ROUTE, h as DEFAULT_API_CATALOG_FORMAT, l as DEFAULT_AGENT_SKILLS_ROUTE_PATTERN, p as DEFAULT_AGENT_SKILL_FORMAT, s as DEFAULT_AGENT_SKILLS_INDEX_FORMAT } from "./standards-discovery-C54V_aJH.mjs";
|
|
3
|
-
import { d as resolveAskAISearchRequestConfig } from "./search-9OnMGMvt.mjs";
|
|
4
|
-
import { a as resolveDocsMetadataBaseUrl } from "./metadata-Dv1ah0Aj.mjs";
|
|
5
|
-
import { c as resolveDocsReviewConfig, o as ensureDocsReviewWorkflow, s as readDocsReviewConfigFromSource, t as runDocsGoldenTasks } from "./agent-evals-f4_OL10T.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-CBZ_JZjf.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 };
|