@open-agent-toolkit/cli 0.3.32 → 0.3.34
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/contributing/documentation.md +1 -0
- package/assets/docs/docs-tooling/check.md +35 -11
- package/assets/docs/docs-tooling/commands.md +20 -2
- package/assets/docs/reference/cli-reference.md +2 -2
- package/assets/docs/reference/config-and-local-state.md +1 -1
- package/assets/docs/reference/configuration.md +1 -1
- package/assets/docs/reference/docs-index-contract.md +3 -2
- package/assets/docs/reference/oat-directory-structure.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-docs-analyze/SKILL.md +35 -11
- package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +2 -2
- package/assets/skills/oat-docs-analyze/references/quality-checklist.md +3 -1
- package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +303 -30
- package/assets/skills/oat-docs-apply/SKILL.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/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +42 -9
- package/dist/commands/docs/check/check.d.ts.map +1 -1
- package/dist/commands/docs/check/check.js +1 -0
- package/dist/commands/docs/check/checker.d.ts +2 -0
- package/dist/commands/docs/check/checker.d.ts.map +1 -1
- package/dist/commands/docs/check/checker.js +6 -1
- package/dist/commands/docs/check/commands.d.ts.map +1 -1
- package/dist/commands/docs/check/commands.js +74 -30
- package/dist/commands/docs/check/files.d.ts +2 -0
- package/dist/commands/docs/check/files.d.ts.map +1 -1
- package/dist/commands/docs/check/files.js +16 -0
- package/dist/commands/docs/check/navigation.d.ts +11 -1
- package/dist/commands/docs/check/navigation.d.ts.map +1 -1
- package/dist/commands/docs/check/navigation.js +40 -69
- 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 +29 -0
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
- package/dist/commands/docs/nav/fumadocs.js +16 -41
- package/dist/commands/docs/nav/membership.d.ts +37 -0
- package/dist/commands/docs/nav/membership.d.ts.map +1 -0
- package/dist/commands/docs/nav/membership.js +474 -0
- package/dist/commands/docs/nav/sync.d.ts +26 -2
- package/dist/commands/docs/nav/sync.d.ts.map +1 -1
- package/dist/commands/docs/nav/sync.js +160 -43
- package/package.json +3 -3
|
@@ -111,6 +111,38 @@ export async function checkCommands(target, pages, report) {
|
|
|
111
111
|
if (!docs.length)
|
|
112
112
|
return;
|
|
113
113
|
const manifests = await packages(target);
|
|
114
|
+
// A framework app's own contributing pages usually speak from the app
|
|
115
|
+
// directory. Its package is tried first; the repository root remains the
|
|
116
|
+
// fallback context, so pages written from the repository root still resolve.
|
|
117
|
+
// Literal Markdown roots keep the repository-root context only.
|
|
118
|
+
const appContext = target.framework !== 'markdown' &&
|
|
119
|
+
target.appRoot !== target.repoRoot &&
|
|
120
|
+
manifests.some((item) => item.root === target.appRoot)
|
|
121
|
+
? target.appRoot
|
|
122
|
+
: undefined;
|
|
123
|
+
const manifestName = (pkg, labelled) => {
|
|
124
|
+
const name = posix(relative(target.repoRoot, join(pkg.root, 'package.json'))) ||
|
|
125
|
+
'package.json';
|
|
126
|
+
if (!labelled)
|
|
127
|
+
return name;
|
|
128
|
+
return pkg.root === target.appRoot
|
|
129
|
+
? `${name} (app root)`
|
|
130
|
+
: pkg.root === target.repoRoot
|
|
131
|
+
? `${name} (repository root)`
|
|
132
|
+
: name;
|
|
133
|
+
};
|
|
134
|
+
const lookup = async (root) => {
|
|
135
|
+
if (!root || !inside(target.repoRoot, root) || !(await present(root)))
|
|
136
|
+
return undefined;
|
|
137
|
+
let current = root;
|
|
138
|
+
while (inside(target.repoRoot, current)) {
|
|
139
|
+
const pkg = manifests.find((item) => item.root === current);
|
|
140
|
+
if (pkg || current === target.repoRoot)
|
|
141
|
+
return pkg;
|
|
142
|
+
current = dirname(current);
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
};
|
|
114
146
|
for (const p of docs)
|
|
115
147
|
for (const node of p.nodes) {
|
|
116
148
|
if (node.type !== 'code' && node.type !== 'inlineCode')
|
|
@@ -119,7 +151,11 @@ export async function checkCommands(target, pages, report) {
|
|
|
119
151
|
node.lang &&
|
|
120
152
|
!['sh', 'shell', 'bash', 'zsh', 'console', 'powershell'].includes(node.lang))
|
|
121
153
|
continue;
|
|
122
|
-
|
|
154
|
+
// Each block starts in the repository-root context and, for framework
|
|
155
|
+
// apps, also in the app-root context; literal `cd` moves both.
|
|
156
|
+
let contexts = appContext
|
|
157
|
+
? [appContext, target.repoRoot]
|
|
158
|
+
: [target.repoRoot];
|
|
123
159
|
const start = node.position?.start;
|
|
124
160
|
const openingLine = p.source.split(/\r?\n/).at((start?.line ?? 1) - 1) ?? '';
|
|
125
161
|
const fenced = node.type === 'code' &&
|
|
@@ -137,36 +173,34 @@ export async function checkCommands(target, pages, report) {
|
|
|
137
173
|
continue;
|
|
138
174
|
const words = tokens(segment);
|
|
139
175
|
if (words[0] === 'cd') {
|
|
140
|
-
cwd
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
: undefined;
|
|
176
|
+
contexts = contexts.map((cwd) => words[1] && !illustrative(words[1]) && cwd
|
|
177
|
+
? resolve(cwd, words[1])
|
|
178
|
+
: undefined);
|
|
144
179
|
continue;
|
|
145
180
|
}
|
|
146
181
|
const manager = words.findIndex((w) => w === 'pnpm' || w === 'npm');
|
|
147
182
|
if (manager < 0)
|
|
148
183
|
continue;
|
|
149
184
|
const kind = words[manager];
|
|
150
|
-
|
|
185
|
+
const moves = [];
|
|
151
186
|
let filter;
|
|
152
187
|
let index = manager + 1;
|
|
153
188
|
while (words[index]?.startsWith('-')) {
|
|
154
189
|
const option = words[index++];
|
|
155
190
|
if (['-w', '--workspace-root'].includes(option))
|
|
156
|
-
|
|
191
|
+
moves.push(() => target.repoRoot);
|
|
157
192
|
else if (option === '--filter' || option === '-F')
|
|
158
193
|
filter = words[index++];
|
|
159
194
|
else if (option.startsWith('--filter='))
|
|
160
195
|
filter = option.slice(9);
|
|
161
196
|
else if (['--dir', '-C', '--prefix'].includes(option)) {
|
|
162
197
|
const dir = words[index++];
|
|
163
|
-
root
|
|
164
|
-
root
|
|
165
|
-
|
|
166
|
-
: undefined;
|
|
198
|
+
moves.push((root) => root && dir && !illustrative(dir)
|
|
199
|
+
? resolve(root, dir)
|
|
200
|
+
: undefined);
|
|
167
201
|
}
|
|
168
202
|
else if (['-r', '--recursive', '--workspace'].includes(option))
|
|
169
|
-
|
|
203
|
+
moves.push(() => undefined);
|
|
170
204
|
}
|
|
171
205
|
let script = words[index];
|
|
172
206
|
if (!script)
|
|
@@ -190,29 +224,32 @@ export async function checkCommands(target, pages, report) {
|
|
|
190
224
|
illustrative(script) ||
|
|
191
225
|
(filter && illustrative(filter)))
|
|
192
226
|
continue;
|
|
193
|
-
|
|
227
|
+
const candidates = [];
|
|
194
228
|
if (filter) {
|
|
195
229
|
const selected = manifests.filter((item) => item.name === filter ||
|
|
196
230
|
(filter.startsWith('./') &&
|
|
197
231
|
posix(relative(target.repoRoot, item.root)) ===
|
|
198
232
|
filter.slice(2)));
|
|
199
233
|
if (selected.length === 1)
|
|
200
|
-
|
|
234
|
+
candidates.push(selected[0]);
|
|
201
235
|
}
|
|
202
|
-
else
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
pkg = manifests.find((item) => item.root === current);
|
|
208
|
-
if (pkg || current === target.repoRoot)
|
|
209
|
-
break;
|
|
210
|
-
current = dirname(current);
|
|
236
|
+
else {
|
|
237
|
+
for (const cwd of contexts) {
|
|
238
|
+
const pkg = await lookup(moves.reduce((root, move) => move(root), cwd));
|
|
239
|
+
if (pkg && !candidates.includes(pkg))
|
|
240
|
+
candidates.push(pkg);
|
|
211
241
|
}
|
|
212
242
|
}
|
|
213
|
-
if (!
|
|
243
|
+
if (!candidates.length) {
|
|
214
244
|
report('commands', 'error', p.path, `Cannot resolve package context for ${kind} ${script}${filter ? ` (--filter ${filter})` : ''}; use literal cd, -w or --filter.`, line);
|
|
215
|
-
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let pkg;
|
|
248
|
+
for (const candidate of candidates) {
|
|
249
|
+
if (Object.hasOwn(candidate.scripts ?? {}, script)) {
|
|
250
|
+
pkg = candidate;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
216
253
|
// pnpm shorthand can invoke an installed local binary. Explicit run
|
|
217
254
|
// remains script-only; never execute a binary or search system PATH.
|
|
218
255
|
const installedBinary = kind === 'pnpm' &&
|
|
@@ -220,12 +257,19 @@ export async function checkCommands(target, pages, report) {
|
|
|
220
257
|
!/[\\/]/.test(script) &&
|
|
221
258
|
script !== '.' &&
|
|
222
259
|
script !== '..' &&
|
|
223
|
-
(await Promise.all([
|
|
224
|
-
if (
|
|
225
|
-
|
|
260
|
+
(await Promise.all([candidate.root, target.repoRoot].map((binRoot) => present(join(binRoot, 'node_modules/.bin', script))))).some(Boolean);
|
|
261
|
+
if (installedBinary) {
|
|
262
|
+
pkg = candidate;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
226
265
|
}
|
|
227
|
-
|
|
228
|
-
report('commands', '
|
|
266
|
+
if (!pkg)
|
|
267
|
+
report('commands', 'error', p.path, `Script "${script}" does not exist in ${candidates
|
|
268
|
+
.map((candidate) => manifestName(candidate, candidates.length > 1))
|
|
269
|
+
.join(' or ')}.`, line);
|
|
270
|
+
else if (Object.hasOwn(pkg.scripts ?? {}, script) &&
|
|
271
|
+
/^\s*echo(?:\s|$)/.test(pkg.scripts[script]))
|
|
272
|
+
report('commands', 'warn', p.path, `Script "${script}" is an echo stub in ${manifestName(pkg, false)}.`, line);
|
|
229
273
|
}
|
|
230
274
|
}
|
|
231
275
|
}
|
|
@@ -11,6 +11,8 @@ export interface CheckTarget {
|
|
|
11
11
|
framework: 'markdown' | 'fumadocs' | 'mkdocs';
|
|
12
12
|
documentation: OatDocumentationConfig;
|
|
13
13
|
matcher: ExclusionMatcher;
|
|
14
|
+
/** Human notices about how an explicit target was interpreted. */
|
|
15
|
+
notices: string[];
|
|
14
16
|
}
|
|
15
17
|
export declare function resolveCheckTarget(cwd: string, targetDir?: string): Promise<CheckTarget>;
|
|
16
18
|
export declare function readerFiles(target: CheckTarget): Promise<string[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/files.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AACD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ1D;AACD,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5D;AAKD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC9C,aAAa,EAAE,sBAAsB,CAAC;IACtC,OAAO,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/files.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AACD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ1D;AACD,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5D;AAKD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC9C,aAAa,EAAE,sBAAsB,CAAC;IACtC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,kEAAkE;IAClE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AACD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAiFtB;AACD,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BxE;AACD,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,CAAC,CAKrB;AACD,+EAA+E;AAC/E,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAMlB;AACD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAU3D"}
|
|
@@ -70,6 +70,16 @@ export async function resolveCheckTarget(cwd, targetDir) {
|
|
|
70
70
|
throw new Error(`Docs content directory does not exist: ${contentRoot}. Set documentation.root or pass --target-dir.`);
|
|
71
71
|
const matchesContent = configuredContentPath !== undefined && configuredContentPath === appRoot;
|
|
72
72
|
const documentation = matching || matchesContent ? doc : {};
|
|
73
|
+
const notices = [];
|
|
74
|
+
if (targetDir !== undefined && configured && doc.root && !matching) {
|
|
75
|
+
const configuredTooling = doc.tooling === 'fumadocs' || doc.tooling === 'mkdocs'
|
|
76
|
+
? doc.tooling
|
|
77
|
+
: undefined;
|
|
78
|
+
if (matchesContent && configuredTooling && framework === 'markdown')
|
|
79
|
+
notices.push(`Notice: --target-dir ${targetDir} is the content directory of the configured ${configuredTooling} app ${doc.root}; it is checked as Markdown, so navigation is skipped. Pass --target-dir ${doc.root} to include navigation.`);
|
|
80
|
+
else if (!matchesContent)
|
|
81
|
+
notices.push(`Notice: --target-dir ${targetDir} is not the configured documentation.root (${doc.root}); checking it as ${framework === 'markdown' ? 'a literal Markdown root' : `a ${framework} app`} without the configured documentation policy. Omit --target-dir to check ${doc.root}.`);
|
|
82
|
+
}
|
|
73
83
|
return {
|
|
74
84
|
repoRoot,
|
|
75
85
|
appRoot,
|
|
@@ -77,6 +87,7 @@ export async function resolveCheckTarget(cwd, targetDir) {
|
|
|
77
87
|
framework,
|
|
78
88
|
documentation,
|
|
79
89
|
matcher: createExclusionMatcher(documentation.nonReaderPaths ?? []),
|
|
90
|
+
notices,
|
|
80
91
|
};
|
|
81
92
|
}
|
|
82
93
|
export async function readerFiles(target) {
|
|
@@ -87,6 +98,11 @@ export async function readerFiles(target) {
|
|
|
87
98
|
});
|
|
88
99
|
for (const e of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
89
100
|
const path = posix(join(dir, e.name));
|
|
101
|
+
// Installed dependencies and hidden tool state (.git, .oat, .next, ...)
|
|
102
|
+
// are never reader content, even under a broad explicit target.
|
|
103
|
+
if (e.isDirectory() &&
|
|
104
|
+
(e.name === 'node_modules' || e.name.startsWith('.')))
|
|
105
|
+
continue;
|
|
90
106
|
if (e.isDirectory() && !isExcludedPath(target.matcher, path, true))
|
|
91
107
|
await walk(path);
|
|
92
108
|
else if (e.isFile() &&
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { Report } from './check.types.js';
|
|
2
2
|
import { type CheckTarget } from './files.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `navOwner: nav-sync` keeps strict generated freshness. Hand-authored and
|
|
5
|
+
* undeclared navigation gets the membership-and-order comparison (issue #367
|
|
6
|
+
* P21/G5): separators, labels, links, rest entries and borrowed pages are the
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkNavigation(target: CheckTarget, report: Report, reported: (ruleId: string, path: string, line?: number) => boolean): Promise<void>;
|
|
4
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,42 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
-
import { buildDocsNavTree } from '../../docs/nav/contents.js';
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { DocsContentsError } from '../../docs/nav/contents.js';
|
|
5
3
|
import { syncFumadocsNavigation } from '../../docs/nav/fumadocs.js';
|
|
4
|
+
import { compareFumadocsMembership, compareMkDocsMembership, } from '../../docs/nav/membership.js';
|
|
6
5
|
import { syncDocsNavigation } from '../../docs/nav/sync.js';
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (typeof item !== 'object' || item === null)
|
|
20
|
-
return item;
|
|
21
|
-
const entry = Object.entries(item)[0];
|
|
22
|
-
if (!entry)
|
|
23
|
-
return item;
|
|
24
|
-
const expectedItem = Array.isArray(expected) ? expected[index] : undefined;
|
|
25
|
-
const children = expectedItem && typeof expectedItem === 'object'
|
|
26
|
-
? Object.values(expectedItem)[0]
|
|
27
|
-
: undefined;
|
|
28
|
-
// The Contents-derived tree identifies sections by their direct landing path.
|
|
29
|
-
// Actual landing/page labels are presentation; explicit group labels remain meaningful.
|
|
30
|
-
const section = Array.isArray(children) &&
|
|
31
|
-
children.some((child) => typeof child === 'string' && child.endsWith('/index.md'));
|
|
32
|
-
return Array.isArray(entry[1])
|
|
33
|
-
? {
|
|
34
|
-
[section ? '$section' : entry[0]]: navIdentity(entry[1], children),
|
|
35
|
-
}
|
|
36
|
-
: entry[1];
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
export async function checkNavigation(target, report) {
|
|
6
|
+
import { inside, posix } from './files.js';
|
|
7
|
+
/**
|
|
8
|
+
* `navOwner: nav-sync` keeps strict generated freshness. Hand-authored and
|
|
9
|
+
* undeclared navigation gets the membership-and-order comparison (issue #367
|
|
10
|
+
* P21/G5): separators, labels, links, rest entries and borrowed pages are the
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
export async function checkNavigation(target, report, reported) {
|
|
40
18
|
if (target.framework === 'markdown')
|
|
41
19
|
return;
|
|
42
20
|
try {
|
|
@@ -48,31 +26,14 @@ export async function checkNavigation(target, report) {
|
|
|
48
26
|
check: true,
|
|
49
27
|
nonReaderPaths,
|
|
50
28
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
typeof actual === 'object' &&
|
|
60
|
-
!Array.isArray(actual) &&
|
|
61
|
-
!Object.hasOwn(actual, 'pages'))
|
|
62
|
-
continue;
|
|
63
|
-
stale =
|
|
64
|
-
!Array.isArray(actual.pages) ||
|
|
65
|
-
!isDeepStrictEqual(actual.pages.map((p) => typeof p === 'string' ? pageIdentity(p) : p), file.meta.pages.map(pageIdentity));
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
report('navigation', 'error', file.path, 'Cannot read or parse meta.json; check file permissions and JSON syntax.');
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (stale)
|
|
73
|
-
report('navigation', 'error', file.path, generated
|
|
74
|
-
? 'Generated navigation is stale; run oat docs nav sync.'
|
|
75
|
-
: 'Navigation membership/order differs from authored Contents; update pages or Contents.');
|
|
29
|
+
if (generated) {
|
|
30
|
+
for (const file of result.metaFiles)
|
|
31
|
+
if (file.changed)
|
|
32
|
+
report('navigation', 'error', file.path, 'Generated navigation is stale; run oat docs nav sync.');
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
for (const issue of await compareFumadocsMembership(target.contentRoot, result.metaFiles))
|
|
36
|
+
report('navigation', 'error', issue.path, issue.message, issue.line);
|
|
76
37
|
}
|
|
77
38
|
for (const path of result.unlisted)
|
|
78
39
|
report('navigation', 'error', path, 'Reader page/folder is unlisted in authored Contents and would be hidden from navigation.');
|
|
@@ -87,18 +48,28 @@ export async function checkNavigation(target, report) {
|
|
|
87
48
|
report('navigation', 'error', path, 'Generated navigation is stale; run oat docs nav sync.');
|
|
88
49
|
}
|
|
89
50
|
else {
|
|
90
|
-
const
|
|
51
|
+
for (const issue of await compareMkDocsMembership({
|
|
52
|
+
appRoot: target.appRoot,
|
|
91
53
|
docsRoot: target.contentRoot,
|
|
92
54
|
nonReaderPaths,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
if (!actual || !Object.hasOwn(actual, 'nav'))
|
|
96
|
-
return;
|
|
97
|
-
if (!isDeepStrictEqual(navIdentity(actual.nav, expected), navIdentity(expected, expected)))
|
|
98
|
-
report('navigation', 'error', 'mkdocs.yml', 'Navigation membership/order differs from authored Contents; update nav or Contents.');
|
|
55
|
+
}))
|
|
56
|
+
report('navigation', 'error', issue.path, issue.message, issue.line);
|
|
99
57
|
}
|
|
100
58
|
}
|
|
101
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
|
+
}
|
|
102
73
|
report('navigation', 'error', 'index.md', error.message);
|
|
103
74
|
}
|
|
104
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"}
|