@farming-labs/docs 0.1.48 → 0.1.50

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.
@@ -5,6 +5,7 @@ import "./api-reference-y7cqtq4w.mjs";
5
5
  import { createFilesystemDocsMcpSource } from "./mcp.mjs";
6
6
  import "./server.mjs";
7
7
  import { a as loadProjectEnv, c as readNavTitle, d as readTopLevelStringProperty, f as resolveDocsConfigPath, i as loadDocsConfigModule, l as readNumberProperty, o as readBooleanProperty, p as resolveDocsContentDir, s as readEnvReferenceProperty, t as extractNestedObjectLiteral, u as readStringProperty } from "./config-C7sUsMkm.mjs";
8
+ import matter from "gray-matter";
8
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
10
  import path from "node:path";
10
11
  import pc from "picocolors";
@@ -212,6 +213,14 @@ function mergeAgentCompactOptions(defaults, overrides) {
212
213
  ...Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== void 0))
213
214
  };
214
215
  }
216
+ function normalizeTokenBudget(value) {
217
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
218
+ return Math.max(1, Math.ceil(value));
219
+ }
220
+ function readPageTokenBudget(pagePath) {
221
+ const { data } = matter(readFileSync(pagePath, "utf-8"));
222
+ return normalizeTokenBudget(data.agent?.tokenBudget);
223
+ }
215
224
  function protectForCompression(input) {
216
225
  const segments = [];
217
226
  const stash = (value) => {
@@ -316,7 +325,10 @@ async function compactAgentDocs(options = {}) {
316
325
  let created = 0;
317
326
  let overwritten = 0;
318
327
  for (const { page, target } of selectedPages) {
319
- const compressed = await compressDocument(renderDocsMarkdownDocument(page), resolvedOptions);
328
+ const sourceDocument = renderDocsMarkdownDocument(page);
329
+ const pageOptions = mergeAgentCompactOptions(resolvedOptions, { maxOutputTokens: readPageTokenBudget(target.pagePath) });
330
+ if (pageOptions.minOutputTokens !== void 0 && pageOptions.maxOutputTokens !== void 0 && pageOptions.minOutputTokens > pageOptions.maxOutputTokens) pageOptions.minOutputTokens = pageOptions.maxOutputTokens;
331
+ const compressed = await compressDocument(sourceDocument, pageOptions);
320
332
  const nextContent = compressed.output.trimEnd();
321
333
  console.log(pc.dim(`Compacting ${page.url} (${compressed.original_input_tokens ?? "?"} -> ${compressed.output_tokens ?? "?"} tokens)...`));
322
334
  if (resolvedOptions.dryRun) continue;
@@ -341,6 +353,9 @@ ${pc.dim("Examples:")}
341
353
  ${pc.cyan("npx @farming-labs/docs@latest agent compact --page installation --page configuration")}
342
354
  ${pc.cyan("npx @farming-labs/docs@latest agent compact --all")}
343
355
 
356
+ ${pc.dim("Per-page override:")}
357
+ Add ${pc.cyan("agent.tokenBudget")} to a page frontmatter block to override the compact output target for that page.
358
+
344
359
  ${pc.dim("Options:")}
345
360
  ${pc.cyan("--all")} Compact every folder-based docs page under the configured contentDir
346
361
  ${pc.cyan("--page <slug|path>")} Add a page explicitly (repeatable); positional page args work too
@@ -75,7 +75,7 @@ async function main() {
75
75
  const { runMcp } = await import("../mcp-CYpMeMfi.mjs");
76
76
  await runMcp(mcpOptions);
77
77
  } else if (parsedCommand.command === "agent" && subcommand === "compact") {
78
- const { compactAgentDocs, parseAgentCompactArgs, printAgentCompactHelp } = await import("../agent-D6Fa41gs.mjs");
78
+ const { compactAgentDocs, parseAgentCompactArgs, printAgentCompactHelp } = await import("../agent-CuYI2Nji.mjs");
79
79
  const agentCompactOptions = parseAgentCompactArgs(args.slice(2));
80
80
  if (agentCompactOptions.help) {
81
81
  printAgentCompactHelp();
@@ -85,11 +85,11 @@ async function main() {
85
85
  } else if (parsedCommand.command === "agent") {
86
86
  console.error(pc.red(`Unknown agent subcommand: ${subcommand ?? "(missing)"}`));
87
87
  console.error();
88
- const { printAgentCompactHelp } = await import("../agent-D6Fa41gs.mjs");
88
+ const { printAgentCompactHelp } = await import("../agent-CuYI2Nji.mjs");
89
89
  printAgentCompactHelp();
90
90
  process.exit(1);
91
91
  } else if (parsedCommand.command === "doctor") {
92
- const { parseDoctorArgs, printDoctorHelp, runDoctor } = await import("../doctor-Jm7T0qYS.mjs");
92
+ const { parseDoctorArgs, printDoctorHelp, runDoctor } = await import("../doctor-D0UYKEFN.mjs");
93
93
  const doctorOptions = parseDoctorArgs(args.slice(1));
94
94
  if (doctorOptions.help) {
95
95
  printDoctorHelp();
@@ -129,7 +129,7 @@ ${pc.dim("Usage:")}
129
129
  ${pc.dim("Commands:")}
130
130
  ${pc.cyan("init")} Scaffold docs in your project (default)
131
131
  ${pc.cyan("agent")} Agent utilities (${pc.dim("compact")} to generate sibling agent.md files)
132
- ${pc.cyan("doctor")} Inspect and score agent-readiness for the current docs app
132
+ ${pc.cyan("doctor")} Inspect and score agent or reader-facing docs quality
133
133
  ${pc.cyan("mcp")} Run the built-in docs MCP server over stdio
134
134
  ${pc.cyan("search")} Search utilities (${pc.dim("sync")} for external indexes)
135
135
  ${pc.cyan("upgrade")} Upgrade @farming-labs/* packages to latest (auto-detect or use --framework)
@@ -162,7 +162,11 @@ ${pc.dim("Options for agent compact:")}
162
162
  ${pc.dim("Options for doctor:")}
163
163
  ${pc.cyan("doctor")} Score the current docs app for agent-readiness
164
164
  ${pc.cyan("doctor --agent")} Same as ${pc.cyan("doctor")}; explicit agent scoring mode
165
+ ${pc.cyan("doctor --site")} Score the current docs app for reader-facing docs quality
166
+ ${pc.cyan("doctor --human")} Alias for ${pc.cyan("doctor --site")}
165
167
  ${pc.cyan("doctor agent")} Subcommand alias for agent scoring
168
+ ${pc.cyan("doctor site")} Subcommand alias for reader-facing scoring
169
+ ${pc.cyan("doctor human")} Legacy alias for reader-facing scoring
166
170
  ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
167
171
 
168
172
  ${pc.dim("Options for search sync:")}
@@ -41,7 +41,11 @@ function parseDoctorArgs(argv) {
41
41
  continue;
42
42
  }
43
43
  if (arg === "--agent" || arg === "agent") {
44
- parsed.agent = true;
44
+ parsed.mode = "agent";
45
+ continue;
46
+ }
47
+ if (arg === "--human" || arg === "human" || arg === "--site" || arg === "site") {
48
+ parsed.mode = "human";
45
49
  continue;
46
50
  }
47
51
  if (arg.startsWith("--config=")) {
@@ -59,7 +63,7 @@ function parseDoctorArgs(argv) {
59
63
  }
60
64
  throw new Error(`Unknown doctor flag or subcommand: ${arg}.`);
61
65
  }
62
- if (!parsed.help && parsed.agent !== true) parsed.agent = true;
66
+ if (!parsed.help && !parsed.mode) parsed.mode = "agent";
63
67
  return parsed;
64
68
  }
65
69
  function printDoctorHelp() {
@@ -69,10 +73,14 @@ ${pc.bold("@farming-labs/docs doctor")}
69
73
  ${pc.dim("Usage:")}
70
74
  pnpm exec docs doctor
71
75
  pnpm exec docs doctor --agent
76
+ pnpm exec docs doctor --site
72
77
  pnpm exec docs doctor agent
78
+ pnpm exec docs doctor site
73
79
 
74
80
  ${pc.dim("Options:")}
75
81
  ${pc.cyan("--agent")} Score agent-readiness for the current docs app (default)
82
+ ${pc.cyan("--site")} Score reader-facing docs quality for the current docs app
83
+ ${pc.cyan("--human")} Alias for ${pc.cyan("--site")}
76
84
  ${pc.cyan("--config <path>")} Use a custom docs config path instead of ${pc.dim("docs.config.ts[x]")}
77
85
  ${pc.cyan("-h, --help")} Show this help message
78
86
  `);
@@ -174,6 +182,46 @@ function resolveAgentFeedbackEnabled(config, content) {
174
182
  if (nestedAgentBlock) return readBooleanProperty(nestedAgentBlock, "enabled") ?? true;
175
183
  return readBooleanProperty(feedbackBlock, "agent") ?? false;
176
184
  }
185
+ function resolveHumanFeedbackEnabled(config, content) {
186
+ const feedback = config?.feedback;
187
+ if (typeof feedback === "boolean") return feedback;
188
+ if (feedback && typeof feedback === "object") return feedback.enabled ?? true;
189
+ const topLevelBoolean = readTopLevelBooleanProperty(content, "feedback");
190
+ if (typeof topLevelBoolean === "boolean") return topLevelBoolean;
191
+ const feedbackBlock = extractNestedObjectLiteral(content, ["feedback"]);
192
+ if (!feedbackBlock) return false;
193
+ return readBooleanProperty(feedbackBlock, "enabled") ?? true;
194
+ }
195
+ function resolveLastUpdatedEnabled(config, content) {
196
+ const current = config?.lastUpdated;
197
+ if (typeof current === "boolean") return current;
198
+ if (current && typeof current === "object") return current.enabled ?? true;
199
+ const topLevelBoolean = readTopLevelBooleanProperty(content, "lastUpdated");
200
+ if (typeof topLevelBoolean === "boolean") return topLevelBoolean;
201
+ const block = extractNestedObjectLiteral(content, ["lastUpdated"]);
202
+ if (!block) return true;
203
+ return readBooleanProperty(block, "enabled") ?? true;
204
+ }
205
+ function hasGithubIntegration(config, content) {
206
+ if (typeof config?.github === "string") return config.github.trim().length > 0;
207
+ if (config?.github && typeof config.github === "object") return typeof config.github.url === "string" && config.github.url.trim().length > 0;
208
+ const topLevelString = readTopLevelStringProperty(content, "github");
209
+ if (typeof topLevelString === "string" && topLevelString.trim().length > 0) return true;
210
+ const githubBlock = extractNestedObjectLiteral(content, ["github"]);
211
+ if (!githubBlock) return false;
212
+ const urlMatch = githubBlock.match(/\burl\s*:\s*["'`]([^"'`]+)["'`]/);
213
+ return typeof urlMatch?.[1] === "string" && urlMatch[1].trim().length > 0;
214
+ }
215
+ function hasReadingTimeSurface(config, content) {
216
+ const current = config?.readingTime;
217
+ if (current === true) return true;
218
+ if (current && typeof current === "object") return current.enabled !== false;
219
+ const topLevelBoolean = readTopLevelBooleanProperty(content, "readingTime");
220
+ if (typeof topLevelBoolean === "boolean") return topLevelBoolean;
221
+ const block = extractNestedObjectLiteral(content, ["readingTime"]);
222
+ if (!block) return false;
223
+ return readBooleanProperty(block, "enabled") ?? true;
224
+ }
177
225
  function hasAgentCompactDefaults(config, content) {
178
226
  if (config?.agent?.compact) return true;
179
227
  return extractNestedObjectLiteral(content, ["agent", "compact"]) !== void 0;
@@ -332,12 +380,84 @@ function metadataScore(descriptionCoverage, relatedCoverage) {
332
380
  score: 0
333
381
  };
334
382
  }
335
- function gradeForScore(score) {
383
+ function descriptionScore(descriptionCoverage) {
384
+ if (descriptionCoverage >= 90) return {
385
+ status: "pass",
386
+ score: 15
387
+ };
388
+ if (descriptionCoverage >= 75) return {
389
+ status: "pass",
390
+ score: 12
391
+ };
392
+ if (descriptionCoverage >= 50) return {
393
+ status: "warn",
394
+ score: 8
395
+ };
396
+ if (descriptionCoverage > 0) return {
397
+ status: "warn",
398
+ score: 4
399
+ };
400
+ return {
401
+ status: "warn",
402
+ score: 0
403
+ };
404
+ }
405
+ function structureScore(structureCoverage) {
406
+ if (structureCoverage >= 90) return {
407
+ status: "pass",
408
+ score: 15
409
+ };
410
+ if (structureCoverage >= 75) return {
411
+ status: "pass",
412
+ score: 12
413
+ };
414
+ if (structureCoverage >= 50) return {
415
+ status: "warn",
416
+ score: 8
417
+ };
418
+ if (structureCoverage > 0) return {
419
+ status: "warn",
420
+ score: 4
421
+ };
422
+ return {
423
+ status: "warn",
424
+ score: 0
425
+ };
426
+ }
427
+ function navigationScore(navigationCoverage) {
428
+ if (navigationCoverage >= 100) return {
429
+ status: "pass",
430
+ score: 15
431
+ };
432
+ if (navigationCoverage >= 80) return {
433
+ status: "pass",
434
+ score: 12
435
+ };
436
+ if (navigationCoverage >= 50) return {
437
+ status: "warn",
438
+ score: 8
439
+ };
440
+ if (navigationCoverage > 0) return {
441
+ status: "warn",
442
+ score: 4
443
+ };
444
+ return {
445
+ status: "fail",
446
+ score: 0
447
+ };
448
+ }
449
+ function gradeForAgentScore(score) {
336
450
  if (score >= 90) return "Agent-optimized";
337
451
  if (score >= 75) return "Agent-ready";
338
452
  if (score >= 60) return "Promising";
339
453
  return "Needs work";
340
454
  }
455
+ function gradeForHumanScore(score) {
456
+ if (score >= 90) return "Human-optimized";
457
+ if (score >= 75) return "Reader-ready";
458
+ if (score >= 60) return "Promising";
459
+ return "Needs work";
460
+ }
341
461
  function formatStatus(status) {
342
462
  if (status === "pass") return pc.green("PASS");
343
463
  if (status === "warn") return pc.yellow("WARN");
@@ -367,6 +487,47 @@ function buildMetadataCoverage(pages) {
367
487
  relatedCoverage: totalPages === 0 ? 0 : Math.round(relatedPages / totalPages * 100)
368
488
  };
369
489
  }
490
+ function countNavigationPages(node) {
491
+ const urls = /* @__PURE__ */ new Set();
492
+ const visit = (current) => {
493
+ if (current.type === "page" && typeof current.url === "string") urls.add(current.url);
494
+ if (current.index && typeof current.index === "object") {
495
+ const indexNode = current.index;
496
+ if (typeof indexNode.url === "string") urls.add(indexNode.url);
497
+ }
498
+ const children = Array.isArray(current.children) ? current.children : [];
499
+ for (const child of children) {
500
+ if (!child || typeof child !== "object") continue;
501
+ visit(child);
502
+ }
503
+ };
504
+ visit(node);
505
+ return urls.size;
506
+ }
507
+ function estimateWordCount(content) {
508
+ return content.match(/\b[\p{L}\p{N}][\p{L}\p{N}'’-]*\b/gu)?.length ?? 0;
509
+ }
510
+ function hasSectionHeadings(content) {
511
+ return /^###{0,1}\s+/m.test(content);
512
+ }
513
+ function buildHumanCoverage(pages, navigationPages) {
514
+ const totalPages = pages.length;
515
+ const describedPages = pages.filter((page) => typeof page.description === "string" && page.description.trim().length > 0).length;
516
+ const longPages = pages.filter((page) => estimateWordCount(page.rawContent ?? "") >= 120).length;
517
+ const structuredLongPages = pages.filter((page) => {
518
+ const content = page.rawContent ?? "";
519
+ return estimateWordCount(content) >= 120 && hasSectionHeadings(content);
520
+ }).length;
521
+ return {
522
+ totalPages,
523
+ describedPages,
524
+ descriptionCoverage: totalPages === 0 ? 0 : Math.round(describedPages / totalPages * 100),
525
+ longPages,
526
+ structuredLongPages,
527
+ structureCoverage: longPages === 0 ? 100 : Math.round(structuredLongPages / longPages * 100),
528
+ navigationPages
529
+ };
530
+ }
370
531
  async function loadDocsConfigModuleWithProjectEnv(rootDir, explicitPath) {
371
532
  const env = loadProjectEnv(rootDir);
372
533
  const injectedKeys = Object.entries(env).filter(([key]) => process.env[key] === void 0).map(([key, value]) => {
@@ -405,7 +566,7 @@ async function inspectAgentReadiness(options = {}) {
405
566
  framework,
406
567
  score: 0,
407
568
  maxScore: 100,
408
- grade: gradeForScore(0),
569
+ grade: gradeForAgentScore(0),
409
570
  checks,
410
571
  coverage: {
411
572
  totalPages: 0,
@@ -472,7 +633,88 @@ async function inspectAgentReadiness(options = {}) {
472
633
  contentDir,
473
634
  score,
474
635
  maxScore,
475
- grade: gradeForScore(score),
636
+ grade: gradeForAgentScore(score),
637
+ checks,
638
+ coverage,
639
+ recommendations: checks.map((check) => check.recommendation).filter((recommendation) => Boolean(recommendation)).slice(0, 3)
640
+ };
641
+ }
642
+ async function inspectHumanReadiness(options = {}) {
643
+ const rootDir = process.cwd();
644
+ const files = listProjectFiles(rootDir);
645
+ const framework = detectFramework(rootDir) ?? detectFrameworkFromFiles(files) ?? "unknown";
646
+ const configCheckMax = 10;
647
+ let configPath;
648
+ try {
649
+ configPath = resolveDocsConfigPath(rootDir, options.configPath);
650
+ } catch (error) {
651
+ const checks = [makeCheck("config", "Docs config", "fail", 0, configCheckMax, error instanceof Error ? error.message : String(error), "Add docs.config.ts[x] or pass --config so the doctor can inspect the docs app.")];
652
+ return {
653
+ mode: "human",
654
+ framework,
655
+ score: 0,
656
+ maxScore: 100,
657
+ grade: gradeForHumanScore(0),
658
+ checks,
659
+ coverage: {
660
+ totalPages: 0,
661
+ describedPages: 0,
662
+ descriptionCoverage: 0,
663
+ longPages: 0,
664
+ structuredLongPages: 0,
665
+ structureCoverage: 0,
666
+ navigationPages: 0
667
+ },
668
+ recommendations: checks.map((check) => check.recommendation).filter(Boolean)
669
+ };
670
+ }
671
+ const configContent = readFileSync(configPath, "utf-8");
672
+ const loadedConfig = await loadDocsConfigModuleWithProjectEnv(rootDir, options.configPath);
673
+ const config = loadedConfig?.config;
674
+ const entry = config?.entry ?? readTopLevelStringProperty(configContent, "entry") ?? "docs";
675
+ const contentDir = config?.contentDir ?? resolveDocsContentDir(rootDir, configContent, entry);
676
+ const ordering = config?.ordering === "alphabetical" || config?.ordering === "numeric" || Array.isArray(config?.ordering) ? config.ordering : void 0;
677
+ const siteTitle = typeof config?.nav?.title === "string" ? config.nav.title : readNavTitle(configContent) ?? "Documentation";
678
+ const searchEnabled = resolveFeatureEnabled(config, configContent, "search");
679
+ const humanFeedbackEnabled = resolveHumanFeedbackEnabled(config, configContent);
680
+ const lastUpdatedEnabled = resolveLastUpdatedEnabled(config, configContent);
681
+ const githubEnabled = hasGithubIntegration(config, configContent);
682
+ const readingTimeEnabled = hasReadingTimeSurface(config, configContent);
683
+ const source = createFilesystemDocsMcpSource({
684
+ rootDir,
685
+ entry,
686
+ contentDir,
687
+ siteTitle,
688
+ ordering
689
+ });
690
+ const coverage = buildHumanCoverage(await Promise.resolve(source.getPages()), countNavigationPages(await Promise.resolve(source.getNavigation())));
691
+ const descriptionResult = descriptionScore(coverage.descriptionCoverage);
692
+ const structureResult = structureScore(coverage.structureCoverage);
693
+ const navigationCoverage = coverage.totalPages === 0 ? 0 : Math.min(100, Math.round(coverage.navigationPages / coverage.totalPages * 100));
694
+ const navigationResult = navigationScore(navigationCoverage);
695
+ const checks = [];
696
+ checks.push(makeCheck("config", "Docs config", "pass", 10, 10, loadedConfig ? `Resolved ${path.relative(rootDir, loadedConfig.path).replace(/\\/g, "/")} and evaluated the config module.` : `Resolved ${path.relative(rootDir, configPath).replace(/\\/g, "/")} using static parsing fallback.`));
697
+ const contentDirAbs = path.resolve(rootDir, contentDir);
698
+ checks.push(coverage.totalPages > 0 ? makeCheck("content", "Docs content", "pass", 15, 15, `Found ${coverage.totalPages} docs page${coverage.totalPages === 1 ? "" : "s"} in ${path.relative(rootDir, contentDirAbs).replace(/\\/g, "/")}.`) : makeCheck("content", "Docs content", "fail", 0, 15, `No folder-based docs pages were found in ${path.relative(rootDir, contentDirAbs).replace(/\\/g, "/")}.`, "Add index/page MDX files under the configured contentDir so the human docs site has pages to render."));
699
+ checks.push(makeCheck("navigation", "Navigation coverage", navigationResult.status, navigationResult.score, 15, coverage.totalPages > 0 ? `The generated docs navigation exposes ${coverage.navigationPages}/${coverage.totalPages} page entries (${navigationCoverage}% coverage).` : "No docs pages were available to score navigation coverage.", navigationCoverage >= 100 ? void 0 : "Make sure every important docs page is reachable from the generated navigation tree and not stranded outside the main docs flow."));
700
+ checks.push(makeCheck("descriptions", "Page descriptions", descriptionResult.status, descriptionResult.score, 15, coverage.totalPages > 0 ? `${coverage.describedPages}/${coverage.totalPages} pages include a description (${coverage.descriptionCoverage}% coverage).` : "No docs pages were available to score descriptions.", coverage.descriptionCoverage >= 75 ? void 0 : "Add frontmatter descriptions to more pages so readers get better search snippets, summaries, and page introductions."));
701
+ checks.push(makeCheck("structure", "Page structure", structureResult.status, structureResult.score, 15, coverage.longPages > 0 ? `${coverage.structuredLongPages}/${coverage.longPages} longer pages include section headings (${coverage.structureCoverage}% coverage).` : "No longer docs pages required section-heading checks.", coverage.structureCoverage >= 75 ? void 0 : "Break longer pages into clearer sections with H2/H3 headings so readers can scan and navigate without hitting a wall of text."));
702
+ checks.push(searchEnabled ? makeCheck("search", "Search surface", "pass", 10, 10, "Search is enabled for the docs site.") : makeCheck("search", "Search surface", "warn", 0, 10, "Search is disabled in docs config.", "Enable search so readers can jump directly to the right page instead of relying only on sidebar browsing."));
703
+ const trustScore = (githubEnabled ? 5 : 0) + (lastUpdatedEnabled ? 5 : 0);
704
+ checks.push(makeCheck("trust", "Trust signals", trustScore === 10 ? "pass" : "warn", trustScore, 10, githubEnabled && lastUpdatedEnabled ? "Edit links and last-updated metadata are configured." : githubEnabled ? "Edit links are configured, but last-updated metadata is not enabled." : lastUpdatedEnabled ? "Last-updated metadata is enabled, but edit links are not configured." : "Edit links and last-updated metadata are not configured.", trustScore === 10 ? void 0 : "Configure GitHub edit links and/or lastUpdated so readers can trust freshness and find the source of truth faster."));
705
+ checks.push(humanFeedbackEnabled ? makeCheck("feedback", "Reader feedback", "pass", 5, 5, "Built-in page feedback is enabled for the docs site.") : makeCheck("feedback", "Reader feedback", "warn", 0, 5, "Built-in page feedback is not enabled.", "Enable feedback if you want readers to leave quick page-level quality signals without opening an issue."));
706
+ checks.push(readingTimeEnabled ? makeCheck("reading-time", "Reading-time cues", "pass", 5, 5, "Reading time is configured for the docs site.") : makeCheck("reading-time", "Reading-time cues", "warn", 0, 5, "Reading time is not enabled.", "Enable readingTime if you want readers to get a quick effort estimate before they dive into longer pages."));
707
+ const score = checks.reduce((total, check) => total + check.score, 0);
708
+ const maxScore = checks.reduce((total, check) => total + check.maxScore, 0);
709
+ return {
710
+ mode: "human",
711
+ framework,
712
+ configPath: path.relative(rootDir, configPath).replace(/\\/g, "/"),
713
+ entry,
714
+ contentDir,
715
+ score,
716
+ maxScore,
717
+ grade: gradeForHumanScore(score),
476
718
  checks,
477
719
  coverage,
478
720
  recommendations: checks.map((check) => check.recommendation).filter((recommendation) => Boolean(recommendation)).slice(0, 3)
@@ -497,7 +739,29 @@ function printAgentDoctorReport(report) {
497
739
  console.log();
498
740
  console.log(pc.dim(`Expected public surfaces: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}, ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE}, ${DEFAULT_LLMS_TXT_ROUTE}, ${DEFAULT_LLMS_FULL_TXT_ROUTE}, ${DEFAULT_SKILL_MD_ROUTE}, ${DEFAULT_MCP_PUBLIC_ROUTE}`));
499
741
  }
742
+ function printHumanDoctorReport(report) {
743
+ console.log(`${pc.bold("@farming-labs/docs doctor")} ${pc.dim("—")} ${pc.bold("site")}`);
744
+ console.log();
745
+ console.log(`${pc.bold("Score:")} ${pc.cyan(`${report.score}/${report.maxScore}`)} ${pc.dim(`(${report.grade})`)}`);
746
+ console.log(`${pc.bold("Framework:")} ${report.framework} ${pc.dim("•")} ${pc.bold("Entry:")} ${report.entry ?? "docs"} ${pc.dim("•")} ${pc.bold("Content:")} ${report.contentDir ?? "-"}`);
747
+ console.log(`${pc.bold("Described pages:")} ${report.coverage.describedPages}/${report.coverage.totalPages} pages ${pc.dim(`(${report.coverage.descriptionCoverage}%)`)}`);
748
+ console.log();
749
+ for (const check of report.checks) {
750
+ console.log(`${formatStatus(check.status)} ${check.title} ${pc.dim(`(${check.score}/${check.maxScore})`)}`);
751
+ console.log(` ${check.detail}`);
752
+ }
753
+ if (report.recommendations.length > 0) {
754
+ console.log();
755
+ console.log(pc.bold("Next steps"));
756
+ for (const recommendation of report.recommendations) console.log(`- ${recommendation}`);
757
+ }
758
+ }
500
759
  async function runDoctor(options = {}) {
760
+ if (options.mode === "human") {
761
+ const report = await inspectHumanReadiness(options);
762
+ printHumanDoctorReport(report);
763
+ return report;
764
+ }
501
765
  const report = await inspectAgentReadiness(options);
502
766
  printAgentDoctorReport(report);
503
767
  return report;
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as SidebarConfig, A as DocsSearchQuery, B as McpDocsSearchConfig, C as DocsSearchAdapter, D as DocsSearchConfig, E as DocsSearchChunkingConfig, F as FeedbackConfig, G as OrderingItem, H as OpenDocsConfig, I as FontStyle, J as PageOpenGraph, K as PageActionsConfig, L as GithubConfig, M as DocsSearchResultType, N as DocsSearchSourcePage, O as DocsSearchDocument, P as DocsTheme, Q as SidebarComponentProps, R as LastUpdatedConfig, S as DocsRelatedItem, T as DocsSearchAdapterFactory, U as OpenDocsProvider, V as OGConfig, W as OpenGraphImage, X as ReadingTimeConfig, Y as PageTwitter, Z as ResolvedDocsRelatedLink, _ as DocsI18nConfig, a as ApiReferenceRenderer, at as ThemeToggleConfig, b as DocsMetadata, c as ChangelogFrontmatter, ct as UIConfig, d as CustomDocsSearchConfig, et as SidebarFolderNode, f as DocsAgentFeedbackContext, g as DocsFeedbackValue, h as DocsFeedbackData, i as ApiReferenceConfig, it as SimpleDocsSearchConfig, j as DocsSearchResult, k as DocsSearchEmbeddingsConfig, l as CodeBlockCopyData, m as DocsConfig, n as AgentFeedbackConfig, nt as SidebarPageNode, o as BreadcrumbConfig, ot as TypesenseDocsSearchConfig, p as DocsAgentFeedbackData, q as PageFrontmatter, r as AlgoliaDocsSearchConfig, rt as SidebarTree, s as ChangelogConfig, st as TypographyConfig, t as AIConfig, tt as SidebarNode, u as CopyMarkdownConfig, v as DocsMcpConfig, w as DocsSearchAdapterContext, x as DocsNav, y as DocsMcpToolsConfig, z as LlmsTxtConfig } from "./types-Boobvv2I.mjs";
1
+ import { $ as SidebarConfig, A as DocsSearchQuery, B as McpDocsSearchConfig, C as DocsSearchAdapter, D as DocsSearchConfig, E as DocsSearchChunkingConfig, F as FeedbackConfig, G as OrderingItem, H as OpenDocsConfig, I as FontStyle, J as PageOpenGraph, K as PageActionsConfig, L as GithubConfig, M as DocsSearchResultType, N as DocsSearchSourcePage, O as DocsSearchDocument, P as DocsTheme, Q as SidebarComponentProps, R as LastUpdatedConfig, S as DocsRelatedItem, T as DocsSearchAdapterFactory, U as OpenDocsProvider, V as OGConfig, W as OpenGraphImage, X as ReadingTimeConfig, Y as PageTwitter, Z as ResolvedDocsRelatedLink, _ as DocsI18nConfig, a as ApiReferenceRenderer, at as ThemeToggleConfig, b as DocsMetadata, c as ChangelogFrontmatter, ct as UIConfig, d as CustomDocsSearchConfig, et as SidebarFolderNode, f as DocsAgentFeedbackContext, g as DocsFeedbackValue, h as DocsFeedbackData, i as ApiReferenceConfig, it as SimpleDocsSearchConfig, j as DocsSearchResult, k as DocsSearchEmbeddingsConfig, l as CodeBlockCopyData, m as DocsConfig, n as AgentFeedbackConfig, nt as SidebarPageNode, o as BreadcrumbConfig, ot as TypesenseDocsSearchConfig, p as DocsAgentFeedbackData, q as PageFrontmatter, r as AlgoliaDocsSearchConfig, rt as SidebarTree, s as ChangelogConfig, st as TypographyConfig, t as AIConfig, tt as SidebarNode, u as CopyMarkdownConfig, v as DocsMcpConfig, w as DocsSearchAdapterContext, x as DocsNav, y as DocsMcpToolsConfig, z as LlmsTxtConfig } from "./types-Agkn2EQE.mjs";
2
2
  import { DocsMcpPage, DocsMcpResolvedConfig } from "./mcp.mjs";
3
- import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-BxJywXXB.mjs";
3
+ import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-DcI00OQT.mjs";
4
4
 
5
5
  //#region src/define-docs.d.ts
6
6
  /**
package/dist/mcp.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as DocsSearchConfig, G as OrderingItem, N as DocsSearchSourcePage, v as DocsMcpConfig } from "./types-Boobvv2I.mjs";
1
+ import { D as DocsSearchConfig, G as OrderingItem, N as DocsSearchSourcePage, v as DocsMcpConfig } from "./types-Agkn2EQE.mjs";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
 
4
4
  //#region src/mcp.d.ts
@@ -1,4 +1,4 @@
1
- import { B as McpDocsSearchConfig, C as DocsSearchAdapter, D as DocsSearchConfig, E as DocsSearchChunkingConfig, N as DocsSearchSourcePage, O as DocsSearchDocument, T as DocsSearchAdapterFactory, d as CustomDocsSearchConfig, j as DocsSearchResult, ot as TypesenseDocsSearchConfig, r as AlgoliaDocsSearchConfig } from "./types-Boobvv2I.mjs";
1
+ import { B as McpDocsSearchConfig, C as DocsSearchAdapter, D as DocsSearchConfig, E as DocsSearchChunkingConfig, N as DocsSearchSourcePage, O as DocsSearchDocument, T as DocsSearchAdapterFactory, d as CustomDocsSearchConfig, j as DocsSearchResult, ot as TypesenseDocsSearchConfig, r as AlgoliaDocsSearchConfig } from "./types-Agkn2EQE.mjs";
2
2
 
3
3
  //#region src/search.d.ts
4
4
  declare function buildDocsSearchDocuments(pages: DocsSearchSourcePage[], chunking?: DocsSearchChunkingConfig): DocsSearchDocument[];
package/dist/server.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { A as DocsSearchQuery, B as McpDocsSearchConfig, C as DocsSearchAdapter, D as DocsSearchConfig, N as DocsSearchSourcePage, O as DocsSearchDocument, T as DocsSearchAdapterFactory, a as ApiReferenceRenderer, j as DocsSearchResult, m as DocsConfig, w as DocsSearchAdapterContext } from "./types-Boobvv2I.mjs";
1
+ import { A as DocsSearchQuery, B as McpDocsSearchConfig, C as DocsSearchAdapter, D as DocsSearchConfig, N as DocsSearchSourcePage, O as DocsSearchDocument, T as DocsSearchAdapterFactory, a as ApiReferenceRenderer, j as DocsSearchResult, m as DocsConfig, w as DocsSearchAdapterContext } from "./types-Agkn2EQE.mjs";
2
2
  import { DocsMcpHttpHandlers, DocsMcpNavigationNode, DocsMcpNavigationTree, DocsMcpPage, DocsMcpResolvedConfig, DocsMcpSource, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio } from "./mcp.mjs";
3
- import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-BxJywXXB.mjs";
3
+ import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-DcI00OQT.mjs";
4
4
 
5
5
  //#region src/api-reference.d.ts
6
6
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
@@ -246,11 +246,20 @@ type DocsRelatedItem = string;
246
246
  interface ResolvedDocsRelatedLink {
247
247
  href: string;
248
248
  }
249
+ interface PageAgentFrontmatter {
250
+ /**
251
+ * Approximate output token target for machine-readable compaction on this page.
252
+ * Used by `docs agent compact` as a per-page override.
253
+ */
254
+ tokenBudget?: number;
255
+ }
249
256
  interface PageFrontmatter {
250
257
  title: string;
251
258
  description?: string;
252
259
  /** Related doc URLs rendered into machine-readable markdown routes and MCP page output. */
253
260
  related?: DocsRelatedItem[];
261
+ /** Per-page agent-oriented metadata used by machine-readable docs features. */
262
+ agent?: PageAgentFrontmatter;
254
263
  /** Override or disable the estimated reading time for this page. */
255
264
  readingTime?: boolean | number;
256
265
  tags?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/docs",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
5
5
  "keywords": [
6
6
  "docs",