@open-agent-toolkit/cli 0.3.33 → 0.3.35
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/README.md +1 -1
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/contributing/documentation.md +1 -0
- package/assets/docs/docs-tooling/check.md +7 -7
- package/assets/docs/reference/docs-index-contract.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-docs-analyze/SKILL.md +1 -1
- package/assets/skills/oat-docs-analyze/references/quality-checklist.md +3 -1
- package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
- package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +6 -1
- package/dist/commands/docs/check/checker.d.ts.map +1 -1
- package/dist/commands/docs/check/checker.js +4 -1
- package/dist/commands/docs/check/mermaid-runtime.d.ts +1 -1
- package/dist/commands/docs/check/mermaid-runtime.d.ts.map +1 -1
- package/dist/commands/docs/check/mermaid-runtime.js +24 -49
- package/dist/commands/docs/check/navigation.d.ts +6 -2
- package/dist/commands/docs/check/navigation.d.ts.map +1 -1
- package/dist/commands/docs/check/navigation.js +21 -1
- package/dist/commands/docs/check/reader-rules.d.ts.map +1 -1
- package/dist/commands/docs/check/reader-rules.js +12 -9
- package/dist/commands/docs/nav/contents.d.ts +50 -2
- package/dist/commands/docs/nav/contents.d.ts.map +1 -1
- package/dist/commands/docs/nav/contents.js +137 -14
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
- package/dist/commands/docs/nav/fumadocs.js +8 -36
- package/dist/commands/docs/nav/membership.d.ts.map +1 -1
- package/dist/commands/docs/nav/membership.js +1 -2
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ For an approved fresh reader model, pass `--ia-preset <preset> --planned-pages <
|
|
|
49
49
|
|
|
50
50
|
Fumadocs static exports include `llms.txt`, `llms-full.txt` and `llms/<page-slugs>/content.md` (root `llms/content.md`) for loaded `.md` readers. These retain authored Markdown and source-relative-link guidance; configured non-readers and MDX are excluded. Use authored Contents maps and `oat docs nav sync`, followed by `--check`, for navigation changes.
|
|
51
51
|
|
|
52
|
-
`oat docs check [--target-dir <dir>] [--rule <id>...] [--json]` performs read-only reader, navigation, contributor-command and Mermaid/SVG checks without a site build. Errors exit 1; warnings exit 0. Configure validated severities, numeric thresholds and reason-bearing exceptions under `documentation.check`. Mermaid syntax parsing lazily
|
|
52
|
+
`oat docs check [--target-dir <dir>] [--rule <id>...] [--json]` performs read-only reader, navigation, contributor-command and Mermaid/SVG checks without a site build. Errors exit 1; warnings exit 0. Configure validated severities, numeric thresholds and reason-bearing exceptions under `documentation.check`. Mermaid syntax parsing lazily loads the pinned Pele parser, which needs no DOM and never renders; Node 22.17 remains the minimum. See [checker rules and configuration](../../apps/oat-docs/docs/docs-tooling/check.md).
|
|
53
53
|
|
|
54
54
|
`oat docs literals [--target-dir <dir>] --page <content-relative-reader.md> --source-root <repo-relative-dir> [--source-exclude <pattern>] --json` supplies read-only inline-code occurrence candidates for a matching triad IA mirror. Repeat each single-value selection flag as needed. Sources stay within narrow explicit implementation roots; searched/skipped evidence and first source physical lines are reported. Once-only strict HTML entity decoding and case-sensitive line-local substring matching do not establish runtime validity; analyze judges canonical relevance and authored IA acceptance. Completed not-found/not-applicable exits 0, input/config errors 1, unexpected failures 2. See [literal helper scope and limits](../../apps/oat-docs/docs/docs-tooling/commands.md#oat-docs-literals).
|
|
55
55
|
|
|
@@ -13,6 +13,7 @@ Documentation should ship with the code it explains. This page covers the core d
|
|
|
13
13
|
- Each `index.md` must include a `## Contents` section.
|
|
14
14
|
- The `## Contents` section is the machine-readable local map for sibling pages and child directories.
|
|
15
15
|
- Use `.md`-suffixed relative links in `## Contents`: `[Page](page.md)` for leaf pages and `[Section](subdir/index.md)` for child directories.
|
|
16
|
+
- A directory whose `index.md` is its only reader page, with no child sections, may instead list same-page heading anchors (`[Setup](#setup)`) as an in-page table of contents; anywhere else, Contents links must target pages or sections.
|
|
16
17
|
- Fumadocs and MkDocs share this authored contract, but they regenerate different artifacts.
|
|
17
18
|
|
|
18
19
|
Within Contents, use dash-only link bullets with optional descriptions. A `### Label` introduces a group containing at least one authored entry; entries before the first group stay ungrouped. `*`/`+` link bullets and empty groups produce file/line errors. The map ends at the next level-two heading, and fenced examples are ignored. A self-index link retains landing-page behavior. Missing Contents retains deterministic manifest fallback, while strict nav still requires maps and reports unlisted reader pages.
|
|
@@ -91,23 +91,23 @@ recursive tabs participate.
|
|
|
91
91
|
|
|
92
92
|
| Rule ID | Default behavior |
|
|
93
93
|
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
94
|
-
| `directory-index` | Error for a reader-bearing directory without `index.md` and a nonempty dash-bullet Contents map, invalid map grammar or unresolved strict Contents target. Ancestors of reader pages participate.
|
|
94
|
+
| `directory-index` | Error for a reader-bearing directory without `index.md` and a nonempty dash-bullet Contents map, invalid map grammar or unresolved strict Contents target. Ancestors of reader pages participate. A same-page anchor such as `#setup` is not a Contents target, except in an index-only leaf. |
|
|
95
95
|
| `root-width` | Flat root Contents links warn above `rootWarn: 10`, error above `rootError: 14`. Real groups exempt the width. |
|
|
96
96
|
| `contents-width` | Ungrouped Contents links warn above `contentsWarn: 10`, error above `contentsError: 15`. `catalog: true` or the recognized OAT generated-index marker exempts only this width rule. |
|
|
97
|
-
| `leaf-size` | Direct reader pages include index. A leaf has no reader-bearing child directory
|
|
97
|
+
| `leaf-size` | Direct reader pages include index. A leaf has no reader-bearing child directory. An index-only leaf (its `index.md` is the only reader page) warns: it renders as a single-page section, so flattening it is a suggestion. Two total pages warn. |
|
|
98
98
|
| `folder-size` | Direct reader pages including index warn above `folderWarn: 12`, error above `folderError: 20`, unless the index has real groups. |
|
|
99
99
|
| `depth` | Content root is depth 0; warn at `depthWarn: 4`, error at `depthError: 5`. |
|
|
100
100
|
| `page-length` | Warn above `pageLines: 500` physical source lines. Explicit `type: reference` or `reference`/`references` path segments use `referenceLines: 1000` and warn above `referenceH3: 40` real H3 headings. No default length error limit. `line` is the first physical line past the line limit, or the first H3 past the heading limit. |
|
|
101
101
|
| `frontmatter` | Unreadable source, invalid YAML or a non-mapping frontmatter document errors. Read failures use relative paths and safe error codes; invalid metadata skips description validation while independent body checks continue at physical positions. |
|
|
102
102
|
| `description` | Missing/empty frontmatter description errors; character lengths below `descriptionMin: 40` or above `descriptionMax: 160` warn. |
|
|
103
103
|
| `h1` | Error unless each page has exactly one real H1. |
|
|
104
|
-
| `links` | Error for unresolved in-root source links/anchors. No network checking. Ordinary escaping links are provenance warnings and their targets are not read. Contents links retain stricter local-map rules.
|
|
104
|
+
| `links` | Error for unresolved in-root source links/anchors. No network checking. Ordinary escaping links are provenance warnings and their targets are not read. Contents links retain stricter local-map rules; the same-page anchors an index-only leaf may list in Contents are checked here, against that page's headings. |
|
|
105
105
|
| `contents-description` | Error for descriptions ending in an ellipsis/truncation. |
|
|
106
106
|
| `overview` | Warn below `overviewWords: 10` before Contents. |
|
|
107
107
|
| `contents-separator` | Warn when Contents descriptions mix separators in a tree. |
|
|
108
108
|
| `navigation` | Error for explicit navigation drift. Nav-sync freshness stays strict; hand-authored/undeclared compare membership and order only (separators, labels, links and borrows are not compared), and absent optional meta/pages/nav skips comparison. Markdown skips it. |
|
|
109
109
|
| `commands` | Error for missing literal contributor scripts; framework app targets try the app package first, then the repository root. Implicit pnpm shorthand also accepts installed package/workspace `.bin` entries. Explicit pnpm run/npm run stays script-only; unresolved context errors; echo scripts warn. |
|
|
110
|
-
| `mermaid` | Parse every real Mermaid fence
|
|
110
|
+
| `mermaid` | Parse every real Mermaid fence with the pinned Pele parser. Invalid syntax errors, with the diagram line and column. |
|
|
111
111
|
| `diagram-pairs` | Diagram light/dark SVG siblings and both page references are required. Conventional diagram paths and explicit paired references participate; ordinary logos are not diagrams. |
|
|
112
112
|
| `diagram-accessibility` | Mermaid missing `accTitle`/`accDescr` warns. SVGs require `role="img"`, nonempty title/desc. Rendered diagrams without page-visible explanatory text or an associated long description warn. |
|
|
113
113
|
| `diagram-freshness` | Warn when changed Mermaid lacks both associated SVG changes; association follows the actual section/tab group. Missing usable Git comparison is explicitly unverified. |
|
|
@@ -140,9 +140,9 @@ binary shorthand may be reported missing. Use `pnpm exec` for explicit binary ex
|
|
|
140
140
|
|
|
141
141
|
## Diagram comparison and limits
|
|
142
142
|
|
|
143
|
-
Syntax validation lazily loads the pinned
|
|
144
|
-
|
|
145
|
-
It does not
|
|
143
|
+
Syntax validation lazily loads the pinned Pele parser, which reads Mermaid
|
|
144
|
+
syntax without a DOM and never renders, runs scripts or loads resources.
|
|
145
|
+
It does not prove semantic/visual correctness. Inspect both themes
|
|
146
146
|
for important diagrams. Put meaningful `accTitle` and `accDescr` in Mermaid,
|
|
147
147
|
keep SVG title/desc and page explanations, and change both theme SVGs with source.
|
|
148
148
|
|
|
@@ -39,6 +39,7 @@ Notes:
|
|
|
39
39
|
- A group with no authored entries is an error with its file and line; add a dash link or remove the empty heading. A `*` or `+` link bullet is also an error with file and line; replace it with `-`.
|
|
40
40
|
- Child directories should link to their `index.md`.
|
|
41
41
|
- Leaf pages should link to their `.md` filename.
|
|
42
|
+
- An index-only leaf, a directory whose `index.md` is its only reader page and that has no child sections, may list same-page heading anchors such as `[Setup](#setup)` as an in-page table of contents. Each must be a valid Markdown link; a destination Markdown renders as plain text, such as `(#set up)`, is an error. Nav sync adds no navigation entry for them, `oat docs check` reports an anchor whose heading does not exist, and the `leaf-size` rule warns that the section could be flattened. In any other directory a same-page anchor in Contents is an error: link a page or section instead.
|
|
42
43
|
- Prose outside `## Contents` is ignored by nav generation and remains freeform; it can explain scope, reader paths, or migration status.
|
|
43
44
|
|
|
44
45
|
## Plain Markdown Ownership
|
|
@@ -6,7 +6,7 @@ disable-model-invocation: true
|
|
|
6
6
|
user-invocable: true
|
|
7
7
|
allowed-tools: Read, Write, Bash(git:*), Bash(oat config:*), Bash(oat docs nav sync --check:*), Bash(oat docs check:*), Bash(oat docs literals:*), Bash(pnpm:*), Bash(bash:*), Bash(jq:*), Bash(node:*), Bash(command -v:*), Bash(sed:*), Bash(npm root -g:*), Bash(test:*), Bash(oat --version), Bash(mkdir:*), Bash(date:*), Bash(echo:*), Bash(cd:*), Bash(pwd:*), Glob, Grep, AskUserQuestion, Task
|
|
8
8
|
metadata:
|
|
9
|
-
version: '1.9.
|
|
9
|
+
version: '1.9.1'
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Docs Analysis
|
|
@@ -15,7 +15,9 @@ Use this checklist when evaluating a docs surface.
|
|
|
15
15
|
- Parent `## Contents` maps include immediate child directories that contain
|
|
16
16
|
docs.
|
|
17
17
|
- Single-page directories still expose a useful `index.md` entrypoint or local
|
|
18
|
-
section map.
|
|
18
|
+
section map. An index-only leaf may list same-page heading anchors in
|
|
19
|
+
`## Contents`; treat it as a flattening suggestion (`leaf-size` warning), not
|
|
20
|
+
a broken map.
|
|
19
21
|
- Asset-only directories are exempt when they contain no Markdown content and
|
|
20
22
|
are not linked as navigable docs sections.
|
|
21
23
|
- `overview.md` is not used as the directory entrypoint.
|
|
@@ -29,7 +29,12 @@ Use file-path-friendly relative Markdown links:
|
|
|
29
29
|
- child directory: `[Section](section/index.md)`;
|
|
30
30
|
- avoid extensionless local docs links for new authored navigation entries;
|
|
31
31
|
- preserve anchors only when the target heading exists and the local renderer
|
|
32
|
-
supports the anchor
|
|
32
|
+
supports the anchor;
|
|
33
|
+
- index-only leaf (its `index.md` is the directory's only reader page and it
|
|
34
|
+
has no child sections): may list same-page heading anchors such as `[Setup](#setup)` as an in-page table
|
|
35
|
+
of contents. They add no navigation entry, and `oat docs check` warns
|
|
36
|
+
(`leaf-size`) that the section could be flattened. Anywhere else a same-page
|
|
37
|
+
anchor in Contents is an error.
|
|
33
38
|
|
|
34
39
|
Existing extensionless links are drift, not precedent. Normalize them only when
|
|
35
40
|
the task scope or an approved recommendation covers that change.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/checker.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,eAAe,CAAC;AAQ1E,gFAAgF;AAChF,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,GAAG,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/checker.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,eAAe,CAAC;AAQ1E,gFAAgF;AAChF,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,GAAG,OAAO,CAAC,eAAe,CAAC,CAiG3B"}
|
|
@@ -55,7 +55,10 @@ export async function checkDocs(options) {
|
|
|
55
55
|
await checkReaders(target, pages, report);
|
|
56
56
|
checkProvenance(target, pages, report);
|
|
57
57
|
if (!options.rules?.length || options.rules.includes('navigation'))
|
|
58
|
-
await checkNavigation(target, report)
|
|
58
|
+
await checkNavigation(target, report, (ruleId, path, line) => findings.some((f) => f.severity === 'error' &&
|
|
59
|
+
f.ruleId === ruleId &&
|
|
60
|
+
f.path === path &&
|
|
61
|
+
f.line === line));
|
|
59
62
|
if (!options.rules?.length || options.rules.includes('commands'))
|
|
60
63
|
await checkCommands(target, pages, report);
|
|
61
64
|
if (!options.rules?.length ||
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Mermaid syntax check with the pinned, DOM-free Pele parser. Never renders. */
|
|
2
2
|
export declare function parseMermaid(source: string): Promise<void>;
|
|
3
3
|
//# sourceMappingURL=mermaid-runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid-runtime.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/mermaid-runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mermaid-runtime.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/mermaid-runtime.ts"],"names":[],"mappings":"AAyBA,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhE"}
|
|
@@ -1,57 +1,32 @@
|
|
|
1
1
|
let runtime;
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const descriptor = saved[index];
|
|
20
|
-
if (descriptor)
|
|
21
|
-
Object.defineProperty(globalThis, key, descriptor);
|
|
22
|
-
else
|
|
23
|
-
Reflect.deleteProperty(globalThis, key);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
2
|
+
/**
|
|
3
|
+
* Pele reports syntax errors in Mermaid's multi-line form: a "Parse error on
|
|
4
|
+
* line N:" header, the offending source line, a caret line and the expected
|
|
5
|
+
* tokens. Keep only the explanation and append the diagram-relative position.
|
|
6
|
+
*/
|
|
7
|
+
function conciseMessage(error) {
|
|
8
|
+
const message = error.snippet
|
|
9
|
+
? error.message.replace(error.snippet, '')
|
|
10
|
+
: error.message;
|
|
11
|
+
const detail = message
|
|
12
|
+
.split('\n')
|
|
13
|
+
.map((line) => line.trim())
|
|
14
|
+
.filter((line) => line && !/^Parse error on line \d+:?$/.test(line))
|
|
15
|
+
.join(' ');
|
|
16
|
+
return error.line > 0
|
|
17
|
+
? `${detail} (diagram line ${error.line}, column ${error.column})`
|
|
18
|
+
: detail;
|
|
26
19
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
20
|
+
/** Mermaid syntax check with the pinned, DOM-free Pele parser. Never renders. */
|
|
21
|
+
export async function parseMermaid(source) {
|
|
22
|
+
runtime ??= import('pele');
|
|
23
|
+
const pele = await runtime;
|
|
31
24
|
try {
|
|
32
|
-
|
|
33
|
-
process.once('beforeExit', () => dom.window.close());
|
|
34
|
-
return { dom, mermaid };
|
|
25
|
+
pele.parse(source);
|
|
35
26
|
}
|
|
36
27
|
catch (error) {
|
|
37
|
-
|
|
28
|
+
if (error instanceof pele.PeleError)
|
|
29
|
+
throw new Error(conciseMessage(error), { cause: error });
|
|
38
30
|
throw error;
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
|
-
/** Lazy, real Node DOM for Mermaid syntax parsing only. Never renders SVG. */
|
|
42
|
-
export async function parseMermaid(source) {
|
|
43
|
-
const result = queue.then(async () => {
|
|
44
|
-
runtime ??= loadRuntime();
|
|
45
|
-
const { dom, mermaid } = await runtime;
|
|
46
|
-
await withDom(dom, async () => {
|
|
47
|
-
mermaid.initialize({
|
|
48
|
-
startOnLoad: false,
|
|
49
|
-
securityLevel: 'strict',
|
|
50
|
-
logLevel: 'fatal',
|
|
51
|
-
});
|
|
52
|
-
await mermaid.parse(source);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
queue = result.catch(() => undefined);
|
|
56
|
-
await result;
|
|
57
|
-
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Report } from './check.types.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type CheckTarget } from './files.js';
|
|
3
3
|
/**
|
|
4
4
|
* `navOwner: nav-sync` keeps strict generated freshness. Hand-authored and
|
|
5
5
|
* undeclared navigation gets the membership-and-order comparison (issue #367
|
|
6
6
|
* P21/G5): separators, labels, links, rest entries and borrowed pages are the
|
|
7
7
|
* author's presentation, never generator output to match.
|
|
8
|
+
*
|
|
9
|
+
* `reported` tells whether an error-severity finding already exists for a
|
|
10
|
+
* rule, path and line. A policy-downgraded finding does not count: the defect
|
|
11
|
+
* still stops navigation, so navigation keeps reporting it as an error.
|
|
8
12
|
*/
|
|
9
|
-
export declare function checkNavigation(target: CheckTarget, report: Report): Promise<void>;
|
|
13
|
+
export declare function checkNavigation(target: CheckTarget, report: Report, reported: (ruleId: string, path: string, line?: number) => boolean): Promise<void>;
|
|
10
14
|
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/navigation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/navigation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE1D;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,GACjE,OAAO,CAAC,IAAI,CAAC,CAyEf"}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { DocsContentsError } from '../../docs/nav/contents.js';
|
|
1
3
|
import { syncFumadocsNavigation } from '../../docs/nav/fumadocs.js';
|
|
2
4
|
import { compareFumadocsMembership, compareMkDocsMembership, } from '../../docs/nav/membership.js';
|
|
3
5
|
import { syncDocsNavigation } from '../../docs/nav/sync.js';
|
|
6
|
+
import { inside, posix } from './files.js';
|
|
4
7
|
/**
|
|
5
8
|
* `navOwner: nav-sync` keeps strict generated freshness. Hand-authored and
|
|
6
9
|
* undeclared navigation gets the membership-and-order comparison (issue #367
|
|
7
10
|
* P21/G5): separators, labels, links, rest entries and borrowed pages are the
|
|
8
11
|
* author's presentation, never generator output to match.
|
|
12
|
+
*
|
|
13
|
+
* `reported` tells whether an error-severity finding already exists for a
|
|
14
|
+
* rule, path and line. A policy-downgraded finding does not count: the defect
|
|
15
|
+
* still stops navigation, so navigation keeps reporting it as an error.
|
|
9
16
|
*/
|
|
10
|
-
export async function checkNavigation(target, report) {
|
|
17
|
+
export async function checkNavigation(target, report, reported) {
|
|
11
18
|
if (target.framework === 'markdown')
|
|
12
19
|
return;
|
|
13
20
|
try {
|
|
@@ -50,6 +57,19 @@ export async function checkNavigation(target, report) {
|
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
catch (error) {
|
|
60
|
+
if (error instanceof DocsContentsError &&
|
|
61
|
+
inside(target.contentRoot, error.indexPath)) {
|
|
62
|
+
// Navigation stops at the first invalid Contents map. directory-index
|
|
63
|
+
// resolves every map the same way and reports each defect at its own
|
|
64
|
+
// index.md, so repeat it here only when that rule did not report it as
|
|
65
|
+
// an error: it was not selected, policy downgraded it, or the defect is
|
|
66
|
+
// framework-only (MkDocs nav accepts only .md targets, while the reader
|
|
67
|
+
// check also accepts .mdx).
|
|
68
|
+
const path = posix(relative(target.contentRoot, error.indexPath));
|
|
69
|
+
if (!reported('directory-index', path, error.line))
|
|
70
|
+
report('navigation', 'error', path, error.message, error.line);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
53
73
|
report('navigation', 'error', 'index.md', error.message);
|
|
54
74
|
}
|
|
55
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader-rules.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/reader-rules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reader-rules.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/reader-rules.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAgWf;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,IAAI,CA+CN"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
2
|
-
import { parseIndexContents, resolveEntryTarget, } from '../../docs/nav/contents.js';
|
|
2
|
+
import { isIndexOnlyLeaf, parseIndexContents, resolveEntryTarget, } from '../../docs/nav/contents.js';
|
|
3
3
|
import { isExcludedPath } from '../../docs/shared/exclusions.js';
|
|
4
4
|
import { DOCS_CHECK_THRESHOLDS } from '../../../config/documentation-check.js';
|
|
5
5
|
import { containedFile, inside, posix, present, readerDirectories, } from './files.js';
|
|
@@ -10,11 +10,13 @@ export async function checkReaders(target, pages, report) {
|
|
|
10
10
|
...target.documentation.check?.thresholds,
|
|
11
11
|
};
|
|
12
12
|
const byPath = new Map(pages.map((p) => [p.path, p]));
|
|
13
|
-
const
|
|
13
|
+
const pagePaths = pages.map((p) => p.path);
|
|
14
|
+
const dirs = readerDirectories(pagePaths);
|
|
14
15
|
const separators = new Map();
|
|
15
16
|
for (const dir of dirs) {
|
|
16
17
|
const path = posix(join(dir, 'index.md'));
|
|
17
18
|
const page = byPath.get(path);
|
|
19
|
+
const indexOnlyLeaf = isIndexOnlyLeaf(dir, pagePaths);
|
|
18
20
|
let grouped = false;
|
|
19
21
|
if (!page)
|
|
20
22
|
report('directory-index', 'error', path, 'Reader-bearing directory needs index.md with a nonempty ## Contents map.');
|
|
@@ -36,7 +38,9 @@ export async function checkReaders(target, pages, report) {
|
|
|
36
38
|
separators.set(entry.separator, { path, line: entry.line });
|
|
37
39
|
try {
|
|
38
40
|
// Existing strict Contents resolution owns local map semantics.
|
|
39
|
-
|
|
41
|
+
// Leaf same-page anchors resolve without error; the links rule
|
|
42
|
+
// below reports any that name a missing heading.
|
|
43
|
+
await resolveEntryTarget(target.contentRoot, join(target.contentRoot, dir), dir, entry, ['.md', '.mdx'], target.matcher, { indexOnlyLeaf });
|
|
40
44
|
}
|
|
41
45
|
catch (error) {
|
|
42
46
|
report('directory-index', 'error', path, String(error.message), entry.line);
|
|
@@ -78,12 +82,11 @@ export async function checkReaders(target, pages, report) {
|
|
|
78
82
|
}
|
|
79
83
|
const count = pages.filter((p) => dirname(p.path) === dir).length;
|
|
80
84
|
const child = dirs.some((d) => d !== dir && (dir === '.' || d.startsWith(`${dir}/`)));
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
85
|
+
// A single-page section renders fine, so its shape is a style suggestion.
|
|
86
|
+
if (indexOnlyLeaf)
|
|
87
|
+
report('leaf-size', 'warn', path, 'Index-only leaf; consider flattening it to a single page or adding reader pages.');
|
|
88
|
+
else if (!child && count === 2)
|
|
89
|
+
report('leaf-size', 'warn', path, 'Leaf has only two direct reader pages including index.md; consider flattening.');
|
|
87
90
|
if (!grouped) {
|
|
88
91
|
if (count > t.folderError)
|
|
89
92
|
report('folder-size', 'error', path, `Folder has ${count} direct reader pages including index; maximum ${t.folderError} without groups.`);
|
|
@@ -5,11 +5,45 @@ export interface DocsContentsEntry {
|
|
|
5
5
|
line?: number;
|
|
6
6
|
description?: string;
|
|
7
7
|
separator?: string;
|
|
8
|
+
/**
|
|
9
|
+
* The authored line from the link's opening `[` onward. `href` is captured
|
|
10
|
+
* only up to the first `)`, so Markdown-level validation reads this instead.
|
|
11
|
+
*/
|
|
12
|
+
source?: string;
|
|
8
13
|
group?: {
|
|
9
14
|
title: string;
|
|
10
15
|
line: number;
|
|
11
16
|
};
|
|
12
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A defect in one index.md Contents map. `indexPath` is the index file that
|
|
20
|
+
* contains it, as the caller named it, and `line` the entry line when the
|
|
21
|
+
* entries were parsed with locations, so a consumer that only sees the thrown
|
|
22
|
+
* error can still report it against the file that contains it.
|
|
23
|
+
*/
|
|
24
|
+
export declare class DocsContentsError extends Error {
|
|
25
|
+
readonly indexPath: string;
|
|
26
|
+
readonly line?: number | undefined;
|
|
27
|
+
constructor(message: string, indexPath: string, line?: number | undefined);
|
|
28
|
+
}
|
|
29
|
+
export interface DocsInventory {
|
|
30
|
+
/** Docs-relative POSIX directory paths, `.` for the docs root. */
|
|
31
|
+
directories: string[];
|
|
32
|
+
/** Docs-relative POSIX page paths. */
|
|
33
|
+
pages: string[];
|
|
34
|
+
}
|
|
35
|
+
/** Reader page extensions: Fumadocs renders `.md` and `.mdx` pages. */
|
|
36
|
+
export declare const PAGE_EXTENSIONS: readonly string[];
|
|
37
|
+
/** Every non-excluded directory and reader page under the docs root. */
|
|
38
|
+
export declare function inventoryDocs(docsRoot: string, matcher: ExclusionMatcher): Promise<DocsInventory>;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a directory is an index-only leaf: its own `index.md` is the only
|
|
41
|
+
* reader page in it or below it, so it renders as a single-page section.
|
|
42
|
+
* `pagePaths` are docs-relative POSIX reader page paths.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isIndexOnlyLeaf(dirRelativePath: string, pagePaths: readonly string[]): boolean;
|
|
45
|
+
/** A Contents href naming a heading on the same page, such as `#setup`. */
|
|
46
|
+
export declare function isSamePageAnchor(href: string): boolean;
|
|
13
47
|
/** Shared discovery lets inventory generation retain its no-map fallback. */
|
|
14
48
|
export declare function hasIndexContents(markdown: string): boolean;
|
|
15
49
|
export declare function parseIndexContents(markdown: string, sourcePath?: string, options?: {
|
|
@@ -22,14 +56,28 @@ interface BuildDocsNavTreeOptions {
|
|
|
22
56
|
dirRelativePath?: string;
|
|
23
57
|
visitedDirectories?: Set<string>;
|
|
24
58
|
nonReaderPaths?: readonly string[];
|
|
59
|
+
/** Reader page inventory; the root call computes it once. */
|
|
60
|
+
pagePaths?: readonly string[];
|
|
25
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve one Contents entry to the page or section it places in navigation.
|
|
64
|
+
*
|
|
65
|
+
* Contents is machine-readable navigation, so every entry must target a page
|
|
66
|
+
* or a section index. The one exception is an index-only leaf: a single-page
|
|
67
|
+
* section may list same-page heading anchors as an in-page table of contents.
|
|
68
|
+
* Those resolve to `anchor` and place nothing in navigation; the `links` check
|
|
69
|
+
* rule validates that each heading exists.
|
|
70
|
+
*/
|
|
26
71
|
export declare function resolveEntryTarget(docsRoot: string, directoryPath: string, dirRelativePath: string, entry: DocsContentsEntry,
|
|
27
72
|
/**
|
|
28
73
|
* Page extensions a Contents link may target. MkDocs renders only `.md`;
|
|
29
74
|
* Fumadocs also renders `.mdx` pages.
|
|
30
75
|
*/
|
|
31
|
-
pageExtensions?: readonly string[], matcher?: ExclusionMatcher
|
|
32
|
-
|
|
76
|
+
pageExtensions?: readonly string[], matcher?: ExclusionMatcher, options?: {
|
|
77
|
+
/** The directory is an index-only leaf (see {@link isIndexOnlyLeaf}). */
|
|
78
|
+
indexOnlyLeaf?: boolean;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
kind: 'page' | 'section' | 'excluded' | 'anchor';
|
|
33
81
|
path: string;
|
|
34
82
|
}>;
|
|
35
83
|
export declare function buildDocsNavTree(options: BuildDocsNavTreeOptions): Promise<DocsNavTree>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/contents.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/contents.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAK1C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAED;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAGxC,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;IAHxB,YACE,OAAO,EAAE,MAAM,EACN,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,YAAA,EAIvB;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,sCAAsC;IACtC,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAAoB,CAAC;AAElE,wEAAwE;AACxE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,aAAa,CAAC,CA8BxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,OAAO,CAUT;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AA+DD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,SAAa,EACvB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1D,iBAAiB,EAAE,CAqErB;AAED,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAE/E,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,6DAA6D;IAC7D,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAMD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,iBAAiB;AACxB;;;GAGG;AACH,cAAc,GAAE,SAAS,MAAM,EAAY,EAC3C,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE;IACP,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;CACpB,GACL,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IACjD,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CAyED;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CAkFtB"}
|
|
@@ -1,7 +1,99 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, join, posix, relative, resolve, } from 'node:path';
|
|
3
3
|
import { createExclusionMatcher, isExcludedPath, } from '../../docs/shared/exclusions.js';
|
|
4
4
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
/**
|
|
8
|
+
* A defect in one index.md Contents map. `indexPath` is the index file that
|
|
9
|
+
* contains it, as the caller named it, and `line` the entry line when the
|
|
10
|
+
* entries were parsed with locations, so a consumer that only sees the thrown
|
|
11
|
+
* error can still report it against the file that contains it.
|
|
12
|
+
*/
|
|
13
|
+
export class DocsContentsError extends Error {
|
|
14
|
+
indexPath;
|
|
15
|
+
line;
|
|
16
|
+
constructor(message, indexPath, line) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.indexPath = indexPath;
|
|
19
|
+
this.line = line;
|
|
20
|
+
this.name = 'DocsContentsError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Reader page extensions: Fumadocs renders `.md` and `.mdx` pages. */
|
|
24
|
+
export const PAGE_EXTENSIONS = ['.md', '.mdx'];
|
|
25
|
+
/** Every non-excluded directory and reader page under the docs root. */
|
|
26
|
+
export async function inventoryDocs(docsRoot, matcher) {
|
|
27
|
+
const directories = [];
|
|
28
|
+
const pages = [];
|
|
29
|
+
async function walk(relativeDir) {
|
|
30
|
+
directories.push(relativeDir);
|
|
31
|
+
const entries = await readdir(join(docsRoot, relativeDir), {
|
|
32
|
+
withFileTypes: true,
|
|
33
|
+
});
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const entryPath = posix.join(relativeDir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
if (matcher.excludesDirectory(entryPath))
|
|
41
|
+
continue;
|
|
42
|
+
await walk(entryPath);
|
|
43
|
+
}
|
|
44
|
+
else if (entry.isFile() &&
|
|
45
|
+
PAGE_EXTENSIONS.includes(extname(entry.name))) {
|
|
46
|
+
if (!matcher.excludesFile(entryPath))
|
|
47
|
+
pages.push(entryPath);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await walk('.');
|
|
52
|
+
directories.sort();
|
|
53
|
+
pages.sort();
|
|
54
|
+
return { directories, pages };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a directory is an index-only leaf: its own `index.md` is the only
|
|
58
|
+
* reader page in it or below it, so it renders as a single-page section.
|
|
59
|
+
* `pagePaths` are docs-relative POSIX reader page paths.
|
|
60
|
+
*/
|
|
61
|
+
export function isIndexOnlyLeaf(dirRelativePath, pagePaths) {
|
|
62
|
+
const prefix = dirRelativePath === '.' ? '' : `${dirRelativePath}/`;
|
|
63
|
+
const ownIndex = `${prefix}index.md`;
|
|
64
|
+
let hasIndex = false;
|
|
65
|
+
for (const path of pagePaths) {
|
|
66
|
+
if (!path.startsWith(prefix))
|
|
67
|
+
continue;
|
|
68
|
+
if (path !== ownIndex)
|
|
69
|
+
return false;
|
|
70
|
+
hasIndex = true;
|
|
71
|
+
}
|
|
72
|
+
return hasIndex;
|
|
73
|
+
}
|
|
74
|
+
/** A Contents href naming a heading on the same page, such as `#setup`. */
|
|
75
|
+
export function isSamePageAnchor(href) {
|
|
76
|
+
return href.trim().startsWith('#');
|
|
77
|
+
}
|
|
78
|
+
const inlineMarkdown = unified().use(remarkParse);
|
|
79
|
+
/**
|
|
80
|
+
* Whether a Contents entry is a real Markdown link to a same-page fragment.
|
|
81
|
+
* The Contents grammar is line-based, so it captures destinations such as
|
|
82
|
+
* `#a b`, `#a(b` or `#a\` that Markdown does not parse as links; those render
|
|
83
|
+
* as plain text and no link rule would ever validate them. The CommonMark
|
|
84
|
+
* parser the reader check uses decides, over the authored link text rather
|
|
85
|
+
* than the `href` truncated at the first `)`, so balanced `#a(b)` and escaped
|
|
86
|
+
* `#a\(b\)` fragments stay links. The links rule then checks the URL remark
|
|
87
|
+
* parsed against the page's headings.
|
|
88
|
+
*/
|
|
89
|
+
function isMarkdownSamePageLink(entry) {
|
|
90
|
+
const tree = inlineMarkdown.parse(entry.source ?? `[${entry.title}](${entry.href})`);
|
|
91
|
+
const paragraph = tree.children?.length === 1 ? tree.children[0] : undefined;
|
|
92
|
+
const link = paragraph?.type === 'paragraph' ? paragraph.children?.[0] : undefined;
|
|
93
|
+
return (link?.type === 'link' &&
|
|
94
|
+
link.position?.start.offset === 0 &&
|
|
95
|
+
(link.url ?? '').startsWith('#'));
|
|
96
|
+
}
|
|
5
97
|
/** Mask fenced examples without changing authored source line numbers. */
|
|
6
98
|
function unfencedLines(markdown) {
|
|
7
99
|
let fence;
|
|
@@ -34,14 +126,14 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
34
126
|
const lines = unfencedLines(markdown);
|
|
35
127
|
const headingIndex = contentsHeadingIndex(lines);
|
|
36
128
|
if (headingIndex < 0) {
|
|
37
|
-
throw new
|
|
129
|
+
throw new DocsContentsError(`Missing required ## Contents section in ${sourcePath}`, sourcePath);
|
|
38
130
|
}
|
|
39
131
|
const entries = [];
|
|
40
132
|
let group;
|
|
41
133
|
let groupEntries = 0;
|
|
42
134
|
const checkGroup = () => {
|
|
43
135
|
if (group && groupEntries === 0) {
|
|
44
|
-
throw new
|
|
136
|
+
throw new DocsContentsError(`${sourcePath}:${group.line}: Contents group "${group.title}" has no entries`, sourcePath);
|
|
45
137
|
}
|
|
46
138
|
};
|
|
47
139
|
for (let i = headingIndex + 1; i < lines.length; i += 1) {
|
|
@@ -56,7 +148,7 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
56
148
|
continue;
|
|
57
149
|
}
|
|
58
150
|
if (/^\s*[*+]\s+\[[^\]]+\]\([^)]+\)/.test(line)) {
|
|
59
|
-
throw new
|
|
151
|
+
throw new DocsContentsError(`${sourcePath}:${i + 1}: Contents links must use a dash (-) bullet`, sourcePath);
|
|
60
152
|
}
|
|
61
153
|
const match = line.match(/^\s*-\s+\[([^\]]+)\]\(([^)]+)\)/);
|
|
62
154
|
if (match) {
|
|
@@ -72,6 +164,7 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
72
164
|
.replace(/^\s*[-–—:]\s*/, '')
|
|
73
165
|
.trim(),
|
|
74
166
|
separator: /^\s*([-–—:])/.exec(line.slice(match[0].length))?.[1],
|
|
167
|
+
source: line.slice(match[0].indexOf('[')),
|
|
75
168
|
}
|
|
76
169
|
: {}),
|
|
77
170
|
});
|
|
@@ -80,22 +173,47 @@ export function parseIndexContents(markdown, sourcePath = 'index.md', options =
|
|
|
80
173
|
}
|
|
81
174
|
checkGroup();
|
|
82
175
|
if (entries.length === 0 && !options.allowEmpty) {
|
|
83
|
-
throw new
|
|
176
|
+
throw new DocsContentsError(`No machine-readable links found under ## Contents in ${sourcePath}`, sourcePath);
|
|
84
177
|
}
|
|
85
178
|
return entries;
|
|
86
179
|
}
|
|
87
180
|
function toPosixPath(path) {
|
|
88
181
|
return path.replaceAll('\\', '/');
|
|
89
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve one Contents entry to the page or section it places in navigation.
|
|
185
|
+
*
|
|
186
|
+
* Contents is machine-readable navigation, so every entry must target a page
|
|
187
|
+
* or a section index. The one exception is an index-only leaf: a single-page
|
|
188
|
+
* section may list same-page heading anchors as an in-page table of contents.
|
|
189
|
+
* Those resolve to `anchor` and place nothing in navigation; the `links` check
|
|
190
|
+
* rule validates that each heading exists.
|
|
191
|
+
*/
|
|
90
192
|
export async function resolveEntryTarget(docsRoot, directoryPath, dirRelativePath, entry,
|
|
91
193
|
/**
|
|
92
194
|
* Page extensions a Contents link may target. MkDocs renders only `.md`;
|
|
93
195
|
* Fumadocs also renders `.mdx` pages.
|
|
94
196
|
*/
|
|
95
|
-
pageExtensions = ['.md'], matcher) {
|
|
197
|
+
pageExtensions = ['.md'], matcher, options = {}) {
|
|
198
|
+
const indexPath = join(docsRoot, dirRelativePath, 'index.md');
|
|
199
|
+
const fail = (message) => {
|
|
200
|
+
throw new DocsContentsError(message, indexPath, entry.line);
|
|
201
|
+
};
|
|
202
|
+
if (isSamePageAnchor(entry.href)) {
|
|
203
|
+
if (!options.indexOnlyLeaf) {
|
|
204
|
+
fail(`Contents link "${entry.href}" in ${indexPath} is a same-page anchor; Contents must link pages or sections unless index.md is the only reader page in its directory and it has no child sections.`);
|
|
205
|
+
}
|
|
206
|
+
if (!isMarkdownSamePageLink(entry)) {
|
|
207
|
+
fail(`Contents link "${entry.href}" in ${indexPath} is not a valid Markdown link; fix its destination (escape or remove spaces and unbalanced parentheses).`);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
kind: 'anchor',
|
|
211
|
+
path: toPosixPath(posix.join(dirRelativePath, 'index.md')),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
96
214
|
const href = entry.href.split('#', 1)[0]?.trim() ?? '';
|
|
97
215
|
if (!href) {
|
|
98
|
-
|
|
216
|
+
fail(`Invalid contents link "${entry.href}" in ${indexPath}`);
|
|
99
217
|
}
|
|
100
218
|
let targetPath = resolve(directoryPath, href);
|
|
101
219
|
if (!href.endsWith('.md')) {
|
|
@@ -108,16 +226,16 @@ pageExtensions = ['.md'], matcher) {
|
|
|
108
226
|
if (relativeTargetPath.startsWith('../') ||
|
|
109
227
|
relativeTargetPath === '..' ||
|
|
110
228
|
relativeTargetPath.length === 0) {
|
|
111
|
-
|
|
229
|
+
fail(`Contents link "${entry.href}" escapes the docs directory from ${indexPath}`);
|
|
112
230
|
}
|
|
113
231
|
if (matcher && isExcludedPath(matcher, relativeTargetPath)) {
|
|
114
232
|
return { kind: 'excluded', path: relativeTargetPath };
|
|
115
233
|
}
|
|
116
234
|
if (!(await fileExists(targetPath))) {
|
|
117
|
-
|
|
235
|
+
fail(`Contents link "${entry.href}" in ${indexPath} does not resolve to a Markdown file`);
|
|
118
236
|
}
|
|
119
237
|
if (!pageExtensions.includes(extname(targetPath))) {
|
|
120
|
-
|
|
238
|
+
fail(`Contents link "${entry.href}" in ${indexPath} must point to a Markdown file or directory index`);
|
|
121
239
|
}
|
|
122
240
|
if (basename(targetPath) === 'index.md') {
|
|
123
241
|
return {
|
|
@@ -139,16 +257,20 @@ export async function buildDocsNavTree(options) {
|
|
|
139
257
|
throw new Error(`Missing required index.md at ${indexPath}`);
|
|
140
258
|
}
|
|
141
259
|
const indexMarkdown = await readFile(indexPath, 'utf8');
|
|
142
|
-
const contentsEntries = parseIndexContents(indexMarkdown, indexPath
|
|
260
|
+
const contentsEntries = parseIndexContents(indexMarkdown, indexPath, {
|
|
261
|
+
locations: true,
|
|
262
|
+
});
|
|
143
263
|
const matcher = createExclusionMatcher(options.nonReaderPaths ?? []);
|
|
264
|
+
const pagePaths = options.pagePaths ?? (await inventoryDocs(options.docsRoot, matcher)).pages;
|
|
265
|
+
const indexOnlyLeaf = isIndexOnlyLeaf(dirRelativePath, pagePaths);
|
|
144
266
|
const nav = dirRelativePath === '.'
|
|
145
267
|
? [{ Home: 'index.md' }]
|
|
146
268
|
: [toPosixPath(join(dirRelativePath, 'index.md'))];
|
|
147
269
|
let currentGroup;
|
|
148
270
|
let groupNav = nav;
|
|
149
271
|
for (const entry of contentsEntries) {
|
|
150
|
-
const resolvedTarget = await resolveEntryTarget(options.docsRoot, directoryPath, dirRelativePath, entry, ['.md'], matcher);
|
|
151
|
-
if (resolvedTarget.kind === 'excluded')
|
|
272
|
+
const resolvedTarget = await resolveEntryTarget(options.docsRoot, directoryPath, dirRelativePath, entry, ['.md'], matcher, { indexOnlyLeaf });
|
|
273
|
+
if (resolvedTarget.kind === 'excluded' || resolvedTarget.kind === 'anchor')
|
|
152
274
|
continue;
|
|
153
275
|
if (resolvedTarget.kind === 'section' &&
|
|
154
276
|
resolvedTarget.path === toPosixPath(join(dirRelativePath, 'index.md'))) {
|
|
@@ -175,6 +297,7 @@ export async function buildDocsNavTree(options) {
|
|
|
175
297
|
dirRelativePath: childDirRelativePath,
|
|
176
298
|
visitedDirectories: childVisitedDirectories,
|
|
177
299
|
nonReaderPaths: options.nonReaderPaths,
|
|
300
|
+
pagePaths,
|
|
178
301
|
}),
|
|
179
302
|
});
|
|
180
303
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fumadocs.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/fumadocs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fumadocs.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/fumadocs.ts"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAMD,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAGlD,CAAC;AACH,eAAO,MAAM,kBAAkB,QACoB,CAAC;AACpD,eAAO,MAAM,aAAa,QAC6D,CAAC;AASxF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQxD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoCvE;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,QAAQ,EAAE,MAAM,EAChB,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,MAAM,CAgBR;AAYD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAOD;;;;;;;;;GASG;AACH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACvC,OAAO,CAUT;AAkLD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,OAAO,CAAC,4BAA4B,CAAC,CA8ExC"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, extname, join, posix } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
4
|
import { createExclusionMatcher, } from '../../docs/shared/exclusions.js';
|
|
5
5
|
import { getFrontmatterBlock, parseFrontmatterScalarFields, } from '../../shared/frontmatter.js';
|
|
6
6
|
import { fileExists } from '../../../fs/io.js';
|
|
7
|
-
import { parseIndexContents, resolveEntryTarget, } from './contents.js';
|
|
8
|
-
const PAGE_EXTENSIONS = ['.md', '.mdx'];
|
|
7
|
+
import { inventoryDocs, isIndexOnlyLeaf, PAGE_EXTENSIONS, parseIndexContents, resolveEntryTarget, } from './contents.js';
|
|
9
8
|
const GROUP_FOLDER = /^\(.+\)$/;
|
|
10
9
|
// The `pages` item grammar of the fumadocs-core 16.10.2 page-tree builder
|
|
11
10
|
// (`resolveFolderItem`): exact rest entries, separators, links, and the
|
|
@@ -22,36 +21,6 @@ const FENCE = /^\s*(```|~~~)/;
|
|
|
22
21
|
function parentDir(path) {
|
|
23
22
|
return posix.dirname(path);
|
|
24
23
|
}
|
|
25
|
-
async function inventoryDocs(docsRoot, matcher) {
|
|
26
|
-
const directories = [];
|
|
27
|
-
const pages = [];
|
|
28
|
-
async function walk(relativeDir) {
|
|
29
|
-
directories.push(relativeDir);
|
|
30
|
-
const entries = await readdir(join(docsRoot, relativeDir), {
|
|
31
|
-
withFileTypes: true,
|
|
32
|
-
});
|
|
33
|
-
for (const entry of entries) {
|
|
34
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const entryPath = posix.join(relativeDir, entry.name);
|
|
38
|
-
if (entry.isDirectory()) {
|
|
39
|
-
if (matcher.excludesDirectory(entryPath))
|
|
40
|
-
continue;
|
|
41
|
-
await walk(entryPath);
|
|
42
|
-
}
|
|
43
|
-
else if (entry.isFile() &&
|
|
44
|
-
PAGE_EXTENSIONS.includes(extname(entry.name))) {
|
|
45
|
-
if (!matcher.excludesFile(entryPath))
|
|
46
|
-
pages.push(entryPath);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
await walk('.');
|
|
51
|
-
directories.sort();
|
|
52
|
-
pages.sort();
|
|
53
|
-
return { directories, pages };
|
|
54
|
-
}
|
|
55
24
|
/**
|
|
56
25
|
* Reduce a Markdown heading to the plain text a sidebar shows: images and
|
|
57
26
|
* links keep their text, code spans keep their content, and emphasis and
|
|
@@ -174,7 +143,8 @@ export function hasImplicitIndex(relativeDir, existing) {
|
|
|
174
143
|
}
|
|
175
144
|
async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex, matcher, inventory, nonIndexUrls) {
|
|
176
145
|
const indexPath = join(docsRoot, relativeDir, 'index.md');
|
|
177
|
-
const entries = parseIndexContents(markdown, indexPath);
|
|
146
|
+
const entries = parseIndexContents(markdown, indexPath, { locations: true });
|
|
147
|
+
const indexOnlyLeaf = isIndexOnlyLeaf(relativeDir, inventory.pages);
|
|
178
148
|
// A folder the loader gives no implicit index (the docs root, a `root: true`
|
|
179
149
|
// folder) must list `index` to show its landing page. Elsewhere, listing
|
|
180
150
|
// `index` would demote the folder index to an ordinary child, so it is left
|
|
@@ -182,8 +152,10 @@ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex,
|
|
|
182
152
|
const pages = implicitIndex ? [] : ['index'];
|
|
183
153
|
let currentGroup;
|
|
184
154
|
for (const entry of entries) {
|
|
185
|
-
const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS, matcher);
|
|
186
|
-
|
|
155
|
+
const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS, matcher, { indexOnlyLeaf });
|
|
156
|
+
// Same-page anchors in an index-only leaf are an in-page table of
|
|
157
|
+
// contents, not navigation.
|
|
158
|
+
if (target.kind === 'excluded' || target.kind === 'anchor')
|
|
187
159
|
continue;
|
|
188
160
|
const targetDir = parentDir(target.path);
|
|
189
161
|
let item;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/membership.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,YAAY,CAAC;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/membership.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,YAAY,CAAC;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAoHD;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAoS/B;AAeD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwEhC"}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { extname, join, posix } from 'node:path';
|
|
3
3
|
import YAML from 'yaml';
|
|
4
|
-
import { buildDocsNavTree } from './contents.js';
|
|
4
|
+
import { buildDocsNavTree, PAGE_EXTENSIONS } from './contents.js';
|
|
5
5
|
import { FUMADOCS_LINK, FUMADOCS_REST_ITEMS, FUMADOCS_SEPARATOR, hasImplicitIndex, pageUrl, } from './fumadocs.js';
|
|
6
|
-
const PAGE_EXTENSIONS = ['.md', '.mdx'];
|
|
7
6
|
const URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
8
7
|
function lineOf(source, needle) {
|
|
9
8
|
const index = source.indexOf(needle);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-agent-toolkit/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.35",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Open Agent Toolkit CLI",
|
|
6
6
|
"homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
|
|
@@ -32,21 +32,19 @@
|
|
|
32
32
|
"commander": "^12.1.0",
|
|
33
33
|
"entities": "6.0.1",
|
|
34
34
|
"github-slugger": "2.0.0",
|
|
35
|
-
"jsdom": "29.1.1",
|
|
36
35
|
"jsonc-parser": "3.2.1",
|
|
37
|
-
"mermaid": "11.12.3",
|
|
38
36
|
"ora": "^9.0.0",
|
|
39
37
|
"parse5": "7.3.0",
|
|
38
|
+
"pele": "1.10.0",
|
|
40
39
|
"remark-gfm": "4.0.1",
|
|
41
40
|
"remark-parse": "11.0.0",
|
|
42
41
|
"unified": "11.0.5",
|
|
43
42
|
"yaml": "2.8.2",
|
|
44
43
|
"zod": "^3.25.76",
|
|
45
|
-
"@open-agent-toolkit/control-plane": "0.3.
|
|
46
|
-
"@open-agent-toolkit/docs-transforms": "0.3.
|
|
44
|
+
"@open-agent-toolkit/control-plane": "0.3.35",
|
|
45
|
+
"@open-agent-toolkit/docs-transforms": "0.3.35"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@types/jsdom": "28.0.3",
|
|
50
48
|
"@types/node": "^22.10.0",
|
|
51
49
|
"tsc-alias": "^1.8.10",
|
|
52
50
|
"tsx": "^4.21.0",
|