dogsbay 0.2.0-beta.11 → 0.2.0-beta.110

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 (182) hide show
  1. package/dist/audit/deliverable-vars.d.ts +18 -0
  2. package/dist/audit/deliverable-vars.js +143 -0
  3. package/dist/audit/dist/crawler.d.ts +23 -0
  4. package/dist/audit/lib/collect-assets.d.ts +7 -0
  5. package/dist/audit/lib/collect-assets.js +64 -0
  6. package/dist/audit/lib/validate-refs.d.ts +37 -0
  7. package/dist/audit/lib/validate-refs.js +132 -0
  8. package/dist/audit/output/json.d.ts +7 -0
  9. package/dist/audit/output/text.d.ts +6 -0
  10. package/dist/audit/registry.d.ts +55 -0
  11. package/dist/audit/rules/seo/description-length.d.ts +16 -0
  12. package/dist/audit/rules/seo/description-missing.d.ts +15 -0
  13. package/dist/audit/rules/seo/dist-meta.d.ts +22 -0
  14. package/dist/audit/rules/seo/h1-brand-keyword.d.ts +2 -0
  15. package/dist/audit/rules/seo/index.d.ts +11 -0
  16. package/dist/audit/rules/seo/json-ld-parse.d.ts +13 -0
  17. package/dist/audit/rules/seo/json-ld-required-fields.d.ts +21 -0
  18. package/dist/audit/rules/seo/keyword-placement.d.ts +2 -0
  19. package/dist/audit/rules/seo/sitemap.d.ts +5 -0
  20. package/dist/audit/rules/seo/sitemap.js +125 -8
  21. package/dist/audit/rules/seo/title-missing.d.ts +20 -0
  22. package/dist/audit/rules/structure/asset-refs.d.ts +23 -0
  23. package/dist/audit/rules/structure/asset-refs.js +41 -0
  24. package/dist/audit/rules/structure/index.d.ts +11 -0
  25. package/dist/audit/rules/structure/index.js +10 -0
  26. package/dist/audit/rules/structure/internal-links.d.ts +22 -0
  27. package/dist/audit/rules/structure/internal-links.js +28 -0
  28. package/dist/audit/rules/structure/locale-coherence.d.ts +17 -0
  29. package/dist/audit/rules/structure/namespace-coherence.d.ts +17 -0
  30. package/dist/audit/rules/structure/nav-target-exists.d.ts +2 -0
  31. package/dist/audit/rules/structure/nav-target-exists.js +107 -0
  32. package/dist/audit/rules/structure/topicmap-valid.d.ts +12 -0
  33. package/dist/audit/rules/structure/topicmap-valid.js +25 -0
  34. package/dist/audit/rules/structure/unresolved-directives.d.ts +2 -0
  35. package/dist/audit/rules/structure/unresolved-directives.js +108 -0
  36. package/dist/audit/rules/structure/version-coherence.d.ts +25 -0
  37. package/dist/audit/run.d.ts +85 -0
  38. package/dist/audit/run.js +4 -0
  39. package/dist/audit/types.d.ts +203 -0
  40. package/dist/commands/add.d.ts +8 -0
  41. package/dist/commands/add.js +1 -1
  42. package/dist/commands/agent-run.js +48 -0
  43. package/dist/commands/agent.d.ts +6 -0
  44. package/dist/commands/chunk.js +121 -0
  45. package/dist/commands/convert.d.ts +64 -0
  46. package/dist/commands/convert.js +656 -52
  47. package/dist/commands/corpus.js +63 -0
  48. package/dist/commands/deliverables.d.ts +7 -0
  49. package/dist/commands/deliverables.js +41 -0
  50. package/dist/commands/embed.js +256 -0
  51. package/dist/commands/enrich.js +192 -0
  52. package/dist/commands/export-techdocs.d.ts +13 -0
  53. package/dist/commands/import-dita.d.ts +23 -0
  54. package/dist/commands/import-dita.js +129 -0
  55. package/dist/commands/import-ditamap.d.ts +37 -0
  56. package/dist/commands/import-ditamap.js +284 -0
  57. package/dist/commands/import-mkdocs.d.ts +9 -0
  58. package/dist/commands/import-mkdocs.js +112 -69
  59. package/dist/commands/index-build.js +59 -0
  60. package/dist/commands/init.d.ts +1 -0
  61. package/dist/commands/lighthouse.d.ts +13 -0
  62. package/dist/commands/migrate-asciidoc.d.ts +72 -0
  63. package/dist/commands/migrate-asciidoc.js +1078 -0
  64. package/dist/commands/migrate-docusaurus.d.ts +44 -0
  65. package/dist/commands/migrate-docusaurus.js +464 -0
  66. package/dist/commands/migrate-fluidtopics-rest.d.ts +21 -0
  67. package/dist/commands/migrate-fluidtopics-rest.js +149 -0
  68. package/dist/commands/migrate-fluidtopics.d.ts +13 -0
  69. package/dist/commands/migrate-fluidtopics.js +158 -0
  70. package/dist/commands/migrate-gitbook.d.ts +11 -0
  71. package/dist/commands/migrate-gitbook.js +296 -0
  72. package/dist/commands/migrate-hugo.d.ts +13 -0
  73. package/dist/commands/migrate-hugo.js +297 -0
  74. package/dist/commands/migrate-mkdocs.d.ts +87 -0
  75. package/dist/commands/migrate-mkdocs.js +1005 -0
  76. package/dist/commands/migrate-myst.d.ts +9 -0
  77. package/dist/commands/migrate-myst.js +239 -0
  78. package/dist/commands/migrate-rst.d.ts +11 -0
  79. package/dist/commands/migrate-rst.js +240 -0
  80. package/dist/commands/preprocess-variants.d.ts +3 -0
  81. package/dist/commands/pull.d.ts +69 -0
  82. package/dist/commands/pull.js +154 -33
  83. package/dist/commands/search.js +144 -0
  84. package/dist/commands/site-build.d.ts +174 -0
  85. package/dist/commands/site-build.js +510 -12
  86. package/dist/commands/site-check.d.ts +35 -0
  87. package/dist/commands/site-check.js +95 -6
  88. package/dist/commands/site-dev.d.ts +30 -0
  89. package/dist/commands/site-dev.js +39 -11
  90. package/dist/commands/site-init.d.ts +44 -0
  91. package/dist/commands/site-init.js +62 -7
  92. package/dist/commands/source.js +777 -0
  93. package/dist/commands/topicmap.d.ts +16 -0
  94. package/dist/commands/topicmap.js +123 -0
  95. package/dist/config/defaults.d.ts +14 -0
  96. package/dist/config/defaults.js +40 -22
  97. package/dist/config/deliverables.d.ts +44 -0
  98. package/dist/config/deliverables.js +68 -0
  99. package/dist/config/find.d.ts +6 -0
  100. package/dist/config/index.d.ts +13 -0
  101. package/dist/config/index.js +2 -1
  102. package/dist/config/load.d.ts +25 -0
  103. package/dist/config/load.js +385 -40
  104. package/dist/config/output.d.ts +2 -0
  105. package/dist/config/schema.d.ts +1082 -0
  106. package/dist/config/schema.js +6 -0
  107. package/dist/config/serialize.d.ts +3 -0
  108. package/dist/config/to-astro-options.d.ts +18 -0
  109. package/dist/config/to-astro-options.js +52 -1
  110. package/dist/corpus/agent-config.js +212 -0
  111. package/dist/corpus/bench.js +694 -0
  112. package/dist/corpus/classify-structured.js +1132 -0
  113. package/dist/corpus/classify.js +331 -0
  114. package/dist/corpus/comparison.js +999 -0
  115. package/dist/corpus/compile-check.js +307 -0
  116. package/dist/corpus/curate.js +808 -0
  117. package/dist/corpus/delta.js +314 -0
  118. package/dist/corpus/endpoints.js +102 -0
  119. package/dist/corpus/exchange.js +189 -0
  120. package/dist/corpus/feed.js +176 -0
  121. package/dist/corpus/git.js +34 -0
  122. package/dist/corpus/init.js +135 -0
  123. package/dist/corpus/judgment-cache.js +109 -0
  124. package/dist/corpus/near-duplicates.js +192 -0
  125. package/dist/corpus/ordering.js +182 -0
  126. package/dist/corpus/pack.js +83 -0
  127. package/dist/corpus/partition.js +0 -0
  128. package/dist/corpus/propose.js +365 -0
  129. package/dist/corpus/pull.js +96 -0
  130. package/dist/corpus/record-validation.js +167 -0
  131. package/dist/corpus/record.js +194 -0
  132. package/dist/corpus/review.js +116 -0
  133. package/dist/corpus/skill.js +444 -0
  134. package/dist/corpus/sources.js +199 -0
  135. package/dist/corpus/store.js +203 -0
  136. package/dist/corpus/update.js +577 -0
  137. package/dist/corpus/versions.js +54 -0
  138. package/dist/engine-cli.d.ts +19 -0
  139. package/dist/engine-cli.js +142 -0
  140. package/dist/formats.d.ts +122 -0
  141. package/dist/formats.js +179 -0
  142. package/dist/import-content.d.ts +185 -0
  143. package/dist/import-content.js +253 -71
  144. package/dist/index.d.ts +2 -0
  145. package/dist/index.js +390 -25
  146. package/dist/lib.d.ts +17 -0
  147. package/dist/lib.js +17 -0
  148. package/dist/passthrough-astro.d.ts +35 -0
  149. package/dist/passthrough-astro.js +152 -0
  150. package/dist/plugins/context.d.ts +36 -0
  151. package/dist/plugins/index.d.ts +11 -0
  152. package/dist/plugins/lifecycle.d.ts +76 -0
  153. package/dist/plugins/loader.d.ts +38 -0
  154. package/dist/registry.d.ts +9 -0
  155. package/dist/registry.js +8 -0
  156. package/dist/resolve-autodoc.d.ts +24 -0
  157. package/dist/resolve-autodoc.js +236 -0
  158. package/dist/site-build/preprocess.d.ts +140 -0
  159. package/dist/site-build/preprocess.js +654 -0
  160. package/dist/source-resolver.d.ts +60 -0
  161. package/dist/topicmap-nav.d.ts +45 -0
  162. package/dist/topicmap-nav.js +112 -0
  163. package/dist/utils/git.d.ts +8 -0
  164. package/dist/utils/git.js +34 -0
  165. package/dist/utils/gitignore.d.ts +8 -0
  166. package/dist/utils/gitignore.js +54 -0
  167. package/dist/utils/link-labels.d.ts +25 -0
  168. package/dist/utils/link-labels.js +67 -0
  169. package/dist/utils/project.d.ts +3 -0
  170. package/dist/utils/remote-includes.d.ts +35 -0
  171. package/dist/utils/remote-includes.js +317 -0
  172. package/dist/versioning/docusaurus.d.ts +53 -0
  173. package/dist/versioning/docusaurus.js +136 -0
  174. package/dist/versioning/lineage.d.ts +107 -0
  175. package/dist/versioning/lineage.js +212 -0
  176. package/package.json +50 -18
  177. package/skills/platform/config-yml/SKILL.md +27 -0
  178. package/skills/platform/frontmatter-fields/SKILL.md +19 -0
  179. package/skills/platform/markdown-directives/SKILL.md +85 -0
  180. package/skills/platform/migration-shape/SKILL.md +314 -0
  181. package/skills/platform/nav-file/SKILL.md +25 -0
  182. package/skills/platform/taxonomy-config/SKILL.md +89 -0
@@ -0,0 +1,18 @@
1
+ import type { Issue } from "./types.js";
2
+ /**
3
+ * Root identifiers referenced by an expression: quoted strings and
4
+ * dotted property tails stripped, reserved words and numbers excluded.
5
+ * `platform == "win" and product.name` → {platform, product}.
6
+ */
7
+ export declare function expressionIdentifiers(expr: string): Set<string>;
8
+ export interface DeliverableVarsOptions {
9
+ /** Resolved source content directories to scan. */
10
+ contentDirs: string[];
11
+ /** The deliverable's merged attribute context. */
12
+ attributes: Record<string, unknown>;
13
+ /** Deliverable name, for the message. */
14
+ deliverable: string;
15
+ /** Paths in issues are made relative to this root. */
16
+ siteRoot: string;
17
+ }
18
+ export declare function auditDeliverableVariables(options: DeliverableVarsOptions): Promise<Issue[]>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Undefined-variable audit for deliverables (plans/deliverables.md
3
+ * Phase 1): preserve-parse every source markdown file, collect the
4
+ * variables referenced by `{{ … }}` / `{% if … %}` / inline spans, and
5
+ * report any not defined by the deliverable's merged attribute context.
6
+ * A `{% if distro == "rosa" %}` whose variable NO deliverable defines is
7
+ * almost certainly a typo — and invisible at build time, because the
8
+ * terminal build treats undefined conditions as falsy and silently drops
9
+ * the block.
10
+ *
11
+ * Command-side helper (does I/O); the result is surfaced through a
12
+ * registered corpus rule closure so it joins the normal audit report and
13
+ * exit-code accounting.
14
+ */
15
+ import { readdir, readFile } from "node:fs/promises";
16
+ import { join, relative } from "node:path";
17
+ import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md";
18
+ /** Words that appear in expressions but are never variables. */
19
+ const EXPR_RESERVED = new Set([
20
+ "true",
21
+ "false",
22
+ "null",
23
+ "none",
24
+ "and",
25
+ "or",
26
+ "not",
27
+ "in",
28
+ "is",
29
+ "defined",
30
+ "undefined",
31
+ ]);
32
+ /**
33
+ * Root identifiers referenced by an expression: quoted strings and
34
+ * dotted property tails stripped, reserved words and numbers excluded.
35
+ * `platform == "win" and product.name` → {platform, product}.
36
+ */
37
+ export function expressionIdentifiers(expr) {
38
+ const out = new Set();
39
+ const stripped = expr
40
+ .replace(/"[^"]*"|'[^']*'/g, " ")
41
+ .replace(/\.[A-Za-z_][A-Za-z0-9_-]*/g, "");
42
+ for (const m of stripped.matchAll(/[A-Za-z_][A-Za-z0-9_-]*/g)) {
43
+ const id = m[0];
44
+ if (!EXPR_RESERVED.has(id.toLowerCase()))
45
+ out.add(id);
46
+ }
47
+ return out;
48
+ }
49
+ function collectInline(nodes, acc) {
50
+ for (const n of nodes ?? []) {
51
+ const t = n.type;
52
+ if (t === "variable") {
53
+ for (const id of expressionIdentifiers(n.expr)) {
54
+ addRef(acc, id, "{{ … }}");
55
+ }
56
+ }
57
+ else if (t === "conditional-span") {
58
+ const span = n;
59
+ for (const id of expressionIdentifiers(span.test))
60
+ addRef(acc, id, "{% if %}");
61
+ collectInline(span.children, acc);
62
+ }
63
+ else {
64
+ const children = n.children;
65
+ if (children)
66
+ collectInline(children, acc);
67
+ }
68
+ }
69
+ }
70
+ function addRef(acc, id, kind) {
71
+ const kinds = acc.refs.get(id) ?? new Set();
72
+ kinds.add(kind);
73
+ acc.refs.set(id, kinds);
74
+ }
75
+ function collectBlocks(nodes, acc) {
76
+ for (const node of nodes) {
77
+ if (node.type === "conditional") {
78
+ for (const id of expressionIdentifiers(String(node.props?.test ?? ""))) {
79
+ addRef(acc, id, "{% if %}");
80
+ }
81
+ }
82
+ else if (node.type === "var-def") {
83
+ acc.defined.add(String(node.props?.name ?? ""));
84
+ for (const id of expressionIdentifiers(String(node.props?.value ?? ""))) {
85
+ addRef(acc, id, "{% set %}");
86
+ }
87
+ }
88
+ if (node.inline)
89
+ collectInline(node.inline, acc);
90
+ if (node.children)
91
+ collectBlocks(node.children, acc);
92
+ }
93
+ }
94
+ async function mdFiles(dir) {
95
+ const out = [];
96
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
97
+ for (const entry of entries) {
98
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
99
+ continue;
100
+ const full = join(dir, entry.name);
101
+ if (entry.isDirectory())
102
+ out.push(...(await mdFiles(full)));
103
+ else if (entry.name.endsWith(".md"))
104
+ out.push(full);
105
+ }
106
+ return out;
107
+ }
108
+ /** hyphen/underscore-insensitive (the preprocessor runs hyphenToUnderscore). */
109
+ const canon = (s) => s.replace(/-/g, "_");
110
+ export async function auditDeliverableVariables(options) {
111
+ const known = new Set(Object.keys(options.attributes).map(canon));
112
+ const issues = [];
113
+ for (const dir of options.contentDirs) {
114
+ for (const file of await mdFiles(dir)) {
115
+ const source = await readFile(file, "utf8").catch(() => null);
116
+ if (source === null)
117
+ continue;
118
+ const acc = { refs: new Map(), defined: new Set() };
119
+ try {
120
+ const { tree } = dogsbayMdToTree(source, { directives: "preserve" });
121
+ collectBlocks(tree, acc);
122
+ }
123
+ catch {
124
+ continue; // unparseable file — other rules own that problem
125
+ }
126
+ for (const [id, kinds] of acc.refs) {
127
+ if (known.has(canon(id)))
128
+ continue;
129
+ if ([...acc.defined].some((d) => canon(d) === canon(id)))
130
+ continue;
131
+ issues.push({
132
+ ruleId: "directives/undefined-variable",
133
+ severity: "warning",
134
+ file: relative(options.siteRoot, file),
135
+ message: `"${id}" (used in ${[...kinds].join(", ")}) is not defined by ` +
136
+ `deliverable "${options.deliverable}" — the terminal build treats ` +
137
+ `undefined conditions as falsy, so guarded content is silently dropped.`,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ return issues;
143
+ }
@@ -0,0 +1,23 @@
1
+ import type { ParsedHtmlFile } from "../types.js";
2
+ export interface CrawlOptions {
3
+ /**
4
+ * File extension to include. Default: `["html", "htm"]`.
5
+ * Excluded files are silently skipped — Pagefind's prebuilt
6
+ * JS bundles, sitemap.xml, robots.txt, OG images, etc. don't
7
+ * need to be parsed for HTML rules.
8
+ */
9
+ extensions?: string[];
10
+ /**
11
+ * Sub-paths to skip (relative to the dist root). Default
12
+ * skips Pagefind's runtime + the `_astro/` build-asset bucket
13
+ * Astro emits — they're not pages, parsing them is wasted
14
+ * work and produces noisy issues.
15
+ */
16
+ excludeDirs?: string[];
17
+ }
18
+ /**
19
+ * Walk `distRoot` recursively, parse every HTML file with
20
+ * cheerio, return the list. The list is sorted by path for
21
+ * deterministic test output.
22
+ */
23
+ export declare function crawlDist(distRoot: string, options?: CrawlOptions): ParsedHtmlFile[];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Return the set of absolute asset paths (leading slash) that
3
+ * `copyAssets` would emit for the given content directories.
4
+ *
5
+ * @param contentDirs Absolute paths, one per source.
6
+ */
7
+ export declare function collectAssets(contentDirs: readonly string[]): Set<string>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Walk one or more content directories collecting absolute-path
3
+ * asset references that the build's `copyAssets` would emit to
4
+ * `public/`. The audit rule `structure/asset-refs` cross-checks
5
+ * `<img src>` / `<a href>` references against this set so a
6
+ * misnamed file shows up as a structured Issue.
7
+ *
8
+ * Mirrors the extension set used by `copyAssets` in
9
+ * `@dogsbay/format-astro/src/project.ts`. Keep in sync — drift
10
+ * would either flag valid refs as broken or miss real misses.
11
+ */
12
+ import { existsSync, readdirSync, statSync } from "node:fs";
13
+ import { join, relative } from "node:path";
14
+ // Same sets as format-astro's copyAssets. Keep in sync.
15
+ const OPTIMIZABLE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
16
+ const PASSTHROUGH_EXTS = new Set([".svg", ".ico", ".pdf"]);
17
+ /**
18
+ * Return the set of absolute asset paths (leading slash) that
19
+ * `copyAssets` would emit for the given content directories.
20
+ *
21
+ * @param contentDirs Absolute paths, one per source.
22
+ */
23
+ export function collectAssets(contentDirs) {
24
+ const out = new Set();
25
+ for (const dir of contentDirs) {
26
+ if (!existsSync(dir))
27
+ continue;
28
+ walk(dir, dir, out);
29
+ }
30
+ return out;
31
+ }
32
+ function walk(rootDir, dir, out) {
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(dir);
36
+ }
37
+ catch {
38
+ return;
39
+ }
40
+ for (const entry of entries) {
41
+ const full = join(dir, entry);
42
+ let isDir = false;
43
+ try {
44
+ isDir = statSync(full).isDirectory();
45
+ }
46
+ catch {
47
+ continue;
48
+ }
49
+ if (isDir) {
50
+ walk(rootDir, full, out);
51
+ continue;
52
+ }
53
+ const dot = entry.lastIndexOf(".");
54
+ if (dot < 0)
55
+ continue;
56
+ const ext = entry.slice(dot).toLowerCase();
57
+ if (!OPTIMIZABLE_EXTS.has(ext) && !PASSTHROUGH_EXTS.has(ext))
58
+ continue;
59
+ const rel = relative(rootDir, full);
60
+ // Public URL is "/<rel>" with forward slashes — copyAssets
61
+ // copies rel-to-source verbatim into public/.
62
+ out.add("/" + rel.replace(/\\/g, "/"));
63
+ }
64
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pure validator for internal page links + asset references in
3
+ * imported content. Walks every `ExportPage`'s tree, collects
4
+ * every absolute `<a href>` and `<img src>`, set-checks against
5
+ * the page slug set + the asset file set.
6
+ *
7
+ * Single source of truth shared between two surfaces:
8
+ * - `structure/internal-links` + `structure/asset-refs` audit
9
+ * rules (warning-or-error issues from `dogsbay site check`)
10
+ * - `dogsbay site build --strict` (throws on first miss)
11
+ *
12
+ * See plans/audit-internal-refs.md.
13
+ */
14
+ import type { ExportPage } from "@dogsbay/types";
15
+ /** One miss found by the validator. */
16
+ export interface RefIssue {
17
+ /** Slug of the page that contains the broken reference. */
18
+ pageSlug: string;
19
+ /** Raw href / src as written by the author. */
20
+ href: string;
21
+ /** Whether the validator was looking for a page or an asset. */
22
+ resolvedAgainst: "page" | "asset";
23
+ }
24
+ export interface ValidateRefsOptions {
25
+ /** Site basePath (e.g. "/docs", "/" for host-root, "" for root). */
26
+ basePath: string;
27
+ /**
28
+ * Absolute paths under the public asset tree, leading slash,
29
+ * matching how authors write them. e.g. `"/_assets/foo.png"`.
30
+ */
31
+ knownAssets: Set<string>;
32
+ }
33
+ export interface ValidateRefsResult {
34
+ linkMisses: RefIssue[];
35
+ assetMisses: RefIssue[];
36
+ }
37
+ export declare function validateRefs(pages: ExportPage[], options: ValidateRefsOptions): ValidateRefsResult;
@@ -0,0 +1,132 @@
1
+ const EXTERNAL_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
2
+ const ANCHOR_ONLY_RE = /^#/;
3
+ const DATA_URI_RE = /^data:/i;
4
+ const ASSET_EXT_RE = /\.(png|jpe?g|gif|webp|svg|avif|pdf|zip|csv|tsv|json|yml|yaml|toml|mp4|webm|mp3|ogg|woff2?|ttf|otf)$/i;
5
+ /**
6
+ * Heuristic for "looks like an asset reference," used to decide
7
+ * whether an `<a href>` should be validated against the asset
8
+ * set or the page set. Image src is always asset; href can be
9
+ * either. Anything ending in a known asset extension is treated
10
+ * as asset.
11
+ */
12
+ function looksLikeAsset(href) {
13
+ // Strip query + fragment before checking extension
14
+ const noFragment = href.split(/[?#]/)[0];
15
+ return ASSET_EXT_RE.test(noFragment);
16
+ }
17
+ export function validateRefs(pages, options) {
18
+ const basePath = options.basePath.replace(/\/$/, "");
19
+ const knownAssets = options.knownAssets;
20
+ // Page slug set — multiple match forms so the validator
21
+ // accepts the common ways authors write the same slug.
22
+ // For slug "tutorials/quickstart" any of these match:
23
+ // /tutorials/quickstart
24
+ // /tutorials/quickstart/
25
+ // /tutorials/quickstart.md
26
+ // /<basePath>/tutorials/quickstart (and the above three)
27
+ const pageSlugs = new Set();
28
+ for (const p of pages) {
29
+ pageSlugs.add(p.slug);
30
+ // Index pages can be referenced as their parent dir
31
+ if (p.slug === "index")
32
+ pageSlugs.add("");
33
+ if (p.slug.endsWith("/index")) {
34
+ pageSlugs.add(p.slug.slice(0, -"/index".length));
35
+ }
36
+ }
37
+ const result = { linkMisses: [], assetMisses: [] };
38
+ for (const page of pages) {
39
+ walkTree(page.tree, (href, kind) => {
40
+ check(href, kind, page.slug, basePath, pageSlugs, knownAssets, result);
41
+ });
42
+ }
43
+ return result;
44
+ }
45
+ /** Internal walk — invokes `visit` for every href/src in the tree. */
46
+ function walkTree(nodes, visit) {
47
+ for (const node of nodes) {
48
+ if (node.inline)
49
+ walkInline(node.inline, visit);
50
+ if (node.html)
51
+ walkHtml(node.html, visit);
52
+ if (node.props?.href && typeof node.props.href === "string") {
53
+ visit(node.props.href, "link");
54
+ }
55
+ if (node.props?.src && typeof node.props.src === "string") {
56
+ visit(node.props.src, "image");
57
+ }
58
+ if (node.children)
59
+ walkTree(node.children, visit);
60
+ }
61
+ }
62
+ function walkInline(nodes, visit) {
63
+ for (const node of nodes) {
64
+ if (node.type === "link" && typeof node.href === "string") {
65
+ visit(node.href, "link");
66
+ if (node.children)
67
+ walkInline(node.children, visit);
68
+ }
69
+ else if (node.type === "image" && typeof node.src === "string") {
70
+ visit(node.src, "image");
71
+ }
72
+ else if (node.type === "highlight" && node.children) {
73
+ walkInline(node.children, visit);
74
+ }
75
+ }
76
+ }
77
+ function walkHtml(html, visit) {
78
+ // Pre-rendered HTML in `node.html` (Starlight importer output,
79
+ // MkDocs raw HTML, etc.) — scan with regexes. Cheap; the audit
80
+ // doesn't need a full HTML parser for this.
81
+ const aRe = /<a\b[^>]*\shref="([^"]+)"/gi;
82
+ let m;
83
+ while ((m = aRe.exec(html)) !== null)
84
+ visit(m[1], "link");
85
+ const imgRe = /<img\b[^>]*\ssrc="([^"]+)"/gi;
86
+ while ((m = imgRe.exec(html)) !== null)
87
+ visit(m[1], "image");
88
+ }
89
+ function check(href, kind, pageSlug, basePath, pageSlugs, knownAssets, result) {
90
+ if (!href || EXTERNAL_RE.test(href))
91
+ return;
92
+ if (ANCHOR_ONLY_RE.test(href))
93
+ return;
94
+ if (DATA_URI_RE.test(href))
95
+ return;
96
+ if (!href.startsWith("/"))
97
+ return; // relative — out of scope for v1
98
+ // Strip query and fragment for the comparison.
99
+ const cleanHref = href.split(/[?#]/)[0];
100
+ // Strip basePath if the author prefixed it. We compare against
101
+ // bare slugs, so `/docs/intro` and `/intro` should both match
102
+ // page slug "intro" (basePath="/docs").
103
+ let path = cleanHref;
104
+ if (basePath && (path === basePath || path.startsWith(`${basePath}/`))) {
105
+ path = path.slice(basePath.length);
106
+ }
107
+ // Strip leading / and trailing /, plus the `.md` extension.
108
+ let trimmed = path.replace(/^\//, "").replace(/\/$/, "");
109
+ trimmed = trimmed.replace(/\.md$/i, "");
110
+ // Image src — always asset.
111
+ // Anchor href — asset if it has an asset extension, else page.
112
+ const wantsAsset = kind === "image" || looksLikeAsset(cleanHref);
113
+ if (wantsAsset) {
114
+ // Asset paths are absolute under the public root.
115
+ // knownAssets stores entries like "/_assets/foo.png".
116
+ const lookup = cleanHref.startsWith("/") && basePath && cleanHref.startsWith(`${basePath}/`)
117
+ ? cleanHref.slice(basePath.length)
118
+ : cleanHref;
119
+ if (!knownAssets.has(lookup)) {
120
+ result.assetMisses.push({
121
+ pageSlug,
122
+ href,
123
+ resolvedAgainst: "asset",
124
+ });
125
+ }
126
+ return;
127
+ }
128
+ // Page lookup
129
+ if (!pageSlugs.has(trimmed)) {
130
+ result.linkMisses.push({ pageSlug, href, resolvedAgainst: "page" });
131
+ }
132
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Machine-readable formatter — emits the full report as JSON.
3
+ * Stable shape for CI consumers; mirrors the `AuditReport` type
4
+ * exactly with no transformations.
5
+ */
6
+ import type { AuditReport } from "../types.js";
7
+ export declare function formatJson(report: AuditReport): string;
@@ -0,0 +1,6 @@
1
+ import type { AuditReport } from "../types.js";
2
+ export interface FormatTextOptions {
3
+ /** Force colour off (e.g. for tests). Defaults to TTY detection. */
4
+ color?: boolean;
5
+ }
6
+ export declare function formatText(report: AuditReport, options?: FormatTextOptions): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Rule registry — the global list of audit rules + helpers for
3
+ * filtering by category, id pattern, or skip list.
4
+ *
5
+ * Categories register their rules at module-load time via
6
+ * `registerRule` (typically inside a category's `index.ts`,
7
+ * imported by the audit runner). The registry is keyed by
8
+ * `Rule.id` so duplicate registrations are caught early.
9
+ *
10
+ * Filtering supports category aliases (`seo`, `links`, …) and
11
+ * glob-style ids (`seo/<name>`, `<cat>/description-<name>`) so
12
+ * the CLI surface can be expressive without each rule needing
13
+ * its own flag. Glob is wildcard-only; see `compileGlob` for
14
+ * the supported pattern grammar.
15
+ */
16
+ import type { Rule, Stage } from "./types.js";
17
+ /**
18
+ * Register a rule. Throws on duplicate id — rule files import
19
+ * each other only via the index, and double-registration
20
+ * indicates a copy-paste bug.
21
+ */
22
+ export declare function registerRule(rule: Rule): void;
23
+ /** All registered rules, in registration order. */
24
+ export declare function listRules(): Rule[];
25
+ /** Lookup by exact id, or `undefined` if unknown. */
26
+ export declare function getRule(id: string): Rule | undefined;
27
+ /** Test-only: clear the registry between tests. Internal use. */
28
+ export declare function _clearRegistry(): void;
29
+ export interface FilterOptions {
30
+ /**
31
+ * Subset of categories to include. When non-empty, rules whose
32
+ * `category` is not in this list are dropped. Empty / undefined
33
+ * means "all categories."
34
+ */
35
+ categories?: string[];
36
+ /**
37
+ * Subset of stages to include. Same semantics as `categories`.
38
+ */
39
+ stages?: Stage[];
40
+ /**
41
+ * Explicit rule list — id strings, optionally with `*` glob.
42
+ * When set, ONLY rules matching one of these patterns run.
43
+ * Takes precedence over `categories` / `stages`.
44
+ */
45
+ rules?: string[];
46
+ /** Patterns of rule ids to skip. Applied AFTER inclusion filters. */
47
+ skip?: string[];
48
+ }
49
+ /**
50
+ * Apply category / stage / explicit / skip filters. Glob patterns
51
+ * use `*` to match any run of characters within a single segment
52
+ * (no recursive descent — pattern grammar is intentionally
53
+ * simple).
54
+ */
55
+ export declare function filterRules(options?: FilterOptions, rules?: Rule[]): Rule[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Rule: `seo/description-length`
3
+ *
4
+ * Flag pages whose `frontmatter.description` exceeds 160
5
+ * characters. Google truncates SERP snippets at ~155-160 chars
6
+ * (the "safe zone" called out in the 100daysofdocs SEO audit);
7
+ * over-length descriptions get cut mid-sentence, leaving readers
8
+ * with a worse first impression than a shorter, polished version.
9
+ *
10
+ * Severity: warning. The page still functions; it just loses
11
+ * some snippet quality. Writers fix it by shortening or by
12
+ * setting `description: ""` to suppress.
13
+ */
14
+ import type { Rule } from "../../types.js";
15
+ export declare const DEFAULT_MAX_DESCRIPTION_LENGTH = 160;
16
+ export declare const descriptionLength: Rule;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Rule: `seo/description-missing`
3
+ *
4
+ * Flag pages without a frontmatter description. Without one,
5
+ * Google falls back to whatever it can extract from body prose,
6
+ * which is often misleading (a navigation chip, a callout title,
7
+ * the first line of a code block). Authoring a description per
8
+ * page is the cheapest fix.
9
+ *
10
+ * Severity: warning. Doesn't break anything — degrades discovery.
11
+ *
12
+ * Skipped for redirect pages: they don't render normal content.
13
+ */
14
+ import type { Rule } from "../../types.js";
15
+ export declare const descriptionMissing: Rule;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Rule: `seo/dist-meta`
3
+ *
4
+ * Walk the rendered HTML and assert the basic SEO meta surface
5
+ * is populated:
6
+ * - `<title>` present and non-empty (error)
7
+ * - `<meta name="description">` present and non-empty (warning)
8
+ * - `<meta property="og:title">` and `og:description` present (warning)
9
+ * - `<meta property="og:type">` set (warning)
10
+ *
11
+ * Twitter Card meta is NOT enforced — Twitter has been steadily
12
+ * deprecating its preview card spec, and og:* covers most cases
13
+ * since Twitter falls back to OG when its own tags are missing.
14
+ *
15
+ * Skipped for redirect pages (we detect them by checking for a
16
+ * <meta http-equiv="refresh"> or a `<link rel="canonical">` to
17
+ * a different URL — but for now, we just exclude `index.html`
18
+ * pages whose body is suspiciously empty). Implementation-wise,
19
+ * a future `seo/redirect-detection` helper could centralize this.
20
+ */
21
+ import type { Rule } from "../../types.js";
22
+ export declare const distMeta: Rule;
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../types.js";
2
+ export declare const h1BrandKeyword: Rule;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Idempotent — calling `registerSeoRules` more than once is a
3
+ * no-op. Lets callers (CLI, tests) register without coordinating.
4
+ */
5
+ export declare function registerSeoRules(): void;
6
+ /**
7
+ * Test-only: reset the "registered" flag so unit tests can
8
+ * register fresh after `_clearRegistry()`. Production code never
9
+ * calls this.
10
+ */
11
+ export declare function _resetSeoRegistration(): void;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Rule: `seo/json-ld-parse`
3
+ *
4
+ * Every `<script type="application/ld+json">` block on the page
5
+ * must parse as valid JSON. Malformed JSON-LD is invisible to
6
+ * Google Search and accidentally introduces invalid output that
7
+ * the Rich Results validator rejects.
8
+ *
9
+ * Severity: error. Broken JSON-LD = wasted machine-readable
10
+ * markup; this is a real bug worth blocking a build on.
11
+ */
12
+ import type { Rule } from "../../types.js";
13
+ export declare const jsonLdParse: Rule;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Rule: `seo/json-ld-required-fields`
3
+ *
4
+ * For each well-formed JSON-LD block on the page, validate that
5
+ * the declared `@type` carries the fields Google expects for
6
+ * Rich Results eligibility. Catches the common case where a
7
+ * writer extends `customJsonLd` with a Course block but omits
8
+ * `provider`, or where DocsLayout's auto-emit drops a required
9
+ * field on a particular page.
10
+ *
11
+ * We don't bundle the full Schema.org vocabulary — just the
12
+ * @type values DocsLayout emits (Article, HowTo, TechArticle,
13
+ * Course) plus a few common extension types (Person,
14
+ * Organization, BreadcrumbList).
15
+ *
16
+ * Companion to `seo/json-ld-parse`: parse fires on malformed
17
+ * JSON; this fires on well-formed JSON that's missing
18
+ * Rich-Results-required fields.
19
+ */
20
+ import type { Rule } from "../../types.js";
21
+ export declare const jsonLdRequiredFields: Rule;
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../../types.js";
2
+ export declare const keywordPlacement: Rule;
@@ -0,0 +1,5 @@
1
+ import type { Rule } from "../../types.js";
2
+ export declare function _resetSitemapCache(): void;
3
+ export declare const sitemapPresent: Rule;
4
+ export declare const sitemapComplete: Rule;
5
+ export declare const sitemapRobotsCoherence: Rule;