@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.
Files changed (47) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/contributing/documentation.md +1 -0
  3. package/assets/docs/docs-tooling/check.md +35 -11
  4. package/assets/docs/docs-tooling/commands.md +20 -2
  5. package/assets/docs/reference/cli-reference.md +2 -2
  6. package/assets/docs/reference/config-and-local-state.md +1 -1
  7. package/assets/docs/reference/configuration.md +1 -1
  8. package/assets/docs/reference/docs-index-contract.md +3 -2
  9. package/assets/docs/reference/oat-directory-structure.md +1 -1
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-docs-analyze/SKILL.md +35 -11
  12. package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +2 -2
  13. package/assets/skills/oat-docs-analyze/references/quality-checklist.md +3 -1
  14. package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +303 -30
  15. package/assets/skills/oat-docs-apply/SKILL.md +3 -1
  16. package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
  17. package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +6 -1
  18. package/dist/commands/config/index.d.ts.map +1 -1
  19. package/dist/commands/config/index.js +42 -9
  20. package/dist/commands/docs/check/check.d.ts.map +1 -1
  21. package/dist/commands/docs/check/check.js +1 -0
  22. package/dist/commands/docs/check/checker.d.ts +2 -0
  23. package/dist/commands/docs/check/checker.d.ts.map +1 -1
  24. package/dist/commands/docs/check/checker.js +6 -1
  25. package/dist/commands/docs/check/commands.d.ts.map +1 -1
  26. package/dist/commands/docs/check/commands.js +74 -30
  27. package/dist/commands/docs/check/files.d.ts +2 -0
  28. package/dist/commands/docs/check/files.d.ts.map +1 -1
  29. package/dist/commands/docs/check/files.js +16 -0
  30. package/dist/commands/docs/check/navigation.d.ts +11 -1
  31. package/dist/commands/docs/check/navigation.d.ts.map +1 -1
  32. package/dist/commands/docs/check/navigation.js +40 -69
  33. package/dist/commands/docs/check/reader-rules.d.ts.map +1 -1
  34. package/dist/commands/docs/check/reader-rules.js +12 -9
  35. package/dist/commands/docs/nav/contents.d.ts +50 -2
  36. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  37. package/dist/commands/docs/nav/contents.js +137 -14
  38. package/dist/commands/docs/nav/fumadocs.d.ts +29 -0
  39. package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
  40. package/dist/commands/docs/nav/fumadocs.js +16 -41
  41. package/dist/commands/docs/nav/membership.d.ts +37 -0
  42. package/dist/commands/docs/nav/membership.d.ts.map +1 -0
  43. package/dist/commands/docs/nav/membership.js +474 -0
  44. package/dist/commands/docs/nav/sync.d.ts +26 -2
  45. package/dist/commands/docs/nav/sync.d.ts.map +1 -1
  46. package/dist/commands/docs/nav/sync.js +160 -43
  47. package/package.json +3 -3
@@ -0,0 +1,474 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { extname, join, posix } from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { buildDocsNavTree, PAGE_EXTENSIONS } from './contents.js';
5
+ import { FUMADOCS_LINK, FUMADOCS_REST_ITEMS, FUMADOCS_SEPARATOR, hasImplicitIndex, pageUrl, } from './fumadocs.js';
6
+ const URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
7
+ function lineOf(source, needle) {
8
+ const index = source.indexOf(needle);
9
+ return index < 0 ? undefined : source.slice(0, index).split('\n').length;
10
+ }
11
+ function isPlainObject(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ async function entryType(docsRoot, path) {
15
+ if (path === '' || path === '..' || path.startsWith('../'))
16
+ return null;
17
+ try {
18
+ const info = await stat(join(docsRoot, path));
19
+ return info.isDirectory() ? 'dir' : info.isFile() ? 'file' : null;
20
+ }
21
+ catch (error) {
22
+ const code = error.code ?? '';
23
+ if (code === 'ENOENT' || code === 'ENOTDIR')
24
+ return null;
25
+ throw error;
26
+ }
27
+ }
28
+ function createTargetResolver(docsRoot) {
29
+ const cache = new Map();
30
+ const resolveTarget = async (key) => {
31
+ const extension = extname(key);
32
+ if (PAGE_EXTENSIONS.includes(extension)) {
33
+ const flat = key.slice(0, -extension.length);
34
+ return {
35
+ id: `page:${flat}`,
36
+ exists: (await entryType(docsRoot, key)) === 'file',
37
+ folder: false,
38
+ url: pageUrl(key),
39
+ };
40
+ }
41
+ if ((await entryType(docsRoot, key)) === 'dir')
42
+ return {
43
+ id: `folder:${key}`,
44
+ exists: true,
45
+ folder: true,
46
+ url: pageUrl(posix.join(key, 'index.md')),
47
+ };
48
+ let exists = false;
49
+ for (const candidate of PAGE_EXTENSIONS)
50
+ if ((await entryType(docsRoot, `${key}${candidate}`)) === 'file')
51
+ exists = true;
52
+ return {
53
+ id: `page:${key}`,
54
+ exists,
55
+ folder: false,
56
+ url: pageUrl(`${key}.md`),
57
+ };
58
+ };
59
+ return (key) => {
60
+ let found = cache.get(key);
61
+ if (!found) {
62
+ found = resolveTarget(key);
63
+ cache.set(key, found);
64
+ }
65
+ return found;
66
+ };
67
+ }
68
+ /** Read one `pages` item with the fumadocs-core page-tree grammar. */
69
+ function classify(dir, item) {
70
+ if (FUMADOCS_REST_ITEMS.has(item))
71
+ return { kind: 'rest' };
72
+ if (FUMADOCS_SEPARATOR.test(item))
73
+ return { kind: 'presentation' };
74
+ const link = FUMADOCS_LINK.exec(item);
75
+ if (link) {
76
+ const url = link.groups?.url ?? '';
77
+ if (link.groups?.external || URL_SCHEME.test(url) || !url.startsWith('/'))
78
+ return { kind: 'link', url: null };
79
+ const path = url.replace(/[?#].*$/, '').replace(/(.)\/+$/, '$1');
80
+ return { kind: 'link', url: path || '/' };
81
+ }
82
+ if (item.startsWith('!'))
83
+ return { kind: 'exclude', key: posix.join(dir, item.slice(1)) };
84
+ // `...folder` inlines a folder's index and children; it never names a page.
85
+ if (item.startsWith('...'))
86
+ return { kind: 'extract', key: posix.join(dir, item.slice(3)) };
87
+ return { kind: 'path', key: posix.join(dir, item) };
88
+ }
89
+ /**
90
+ * Compare present Fumadocs `meta.json` files with the Contents-derived
91
+ * expectation in `metaFiles` (from a `check` run of nav sync).
92
+ *
93
+ * A Contents entry is reachable when its folder's `pages` lists it, a rest
94
+ * entry shows it, any other `pages` entry borrows it, or a root-relative link
95
+ * item points at its URL. An absent meta.json or one without `pages` uses the
96
+ * loader's folder order and is skipped.
97
+ */
98
+ export async function compareFumadocsMembership(docsRoot, metaFiles) {
99
+ const issues = [];
100
+ const actuals = [];
101
+ for (const file of metaFiles) {
102
+ let source;
103
+ let parsed;
104
+ try {
105
+ source = await readFile(join(docsRoot, file.path), 'utf8');
106
+ }
107
+ catch (error) {
108
+ if (error.code === 'ENOENT')
109
+ continue;
110
+ source = '';
111
+ }
112
+ try {
113
+ parsed = JSON.parse(source);
114
+ }
115
+ catch {
116
+ issues.push({
117
+ path: file.path,
118
+ kind: 'unreadable',
119
+ message: 'Cannot read or parse meta.json; check file permissions and JSON syntax.',
120
+ });
121
+ continue;
122
+ }
123
+ if (isPlainObject(parsed) && !Object.hasOwn(parsed, 'pages'))
124
+ continue;
125
+ const pages = isPlainObject(parsed) ? parsed.pages : undefined;
126
+ if (!Array.isArray(pages) ||
127
+ pages.some((item) => typeof item !== 'string')) {
128
+ issues.push({
129
+ path: file.path,
130
+ kind: 'malformed',
131
+ message: 'meta.json "pages" must be an array of strings; fix it or remove "pages" to use folder order.',
132
+ line: lineOf(source, '"pages"'),
133
+ });
134
+ continue;
135
+ }
136
+ const dir = posix.dirname(file.path);
137
+ const pagesIndex = parsed.pagesIndex;
138
+ actuals.push({
139
+ file,
140
+ dir,
141
+ source,
142
+ metadata: parsed,
143
+ ...(typeof pagesIndex === 'string' && pagesIndex.trim()
144
+ ? { pagesIndex: pagesIndex.trim() }
145
+ : {}),
146
+ items: pages.map((raw) => ({
147
+ raw,
148
+ item: classify(dir, raw),
149
+ })),
150
+ });
151
+ }
152
+ const resolveTarget = createTargetResolver(docsRoot);
153
+ for (const actual of actuals)
154
+ for (const { raw, item } of actual.items) {
155
+ if (item.kind === 'extract') {
156
+ if (!(await resolveTarget(item.key)).folder)
157
+ issues.push({
158
+ path: actual.file.path,
159
+ kind: 'dangling',
160
+ message: `Missing target: pages entry "${raw}" extracts no folder; "..." inlines a folder's pages, so list a page by its name instead.`,
161
+ line: lineOf(actual.source, JSON.stringify(raw)),
162
+ });
163
+ continue;
164
+ }
165
+ if (item.kind !== 'path')
166
+ continue;
167
+ if (!(await resolveTarget(item.key)).exists)
168
+ issues.push({
169
+ path: actual.file.path,
170
+ kind: 'dangling',
171
+ message: `Missing target: pages entry "${raw}" names no page or folder; remove it or restore the page.`,
172
+ line: lineOf(actual.source, JSON.stringify(raw)),
173
+ });
174
+ }
175
+ // Walk the sidebar the way fumadocs-core 16.10.2 `buildFolder` does, from
176
+ // the docs root through attached folders only: a meta.json in a folder the
177
+ // tree never attaches contributes nothing, and a URL link to a folder's
178
+ // landing page reaches that page but does not attach the folder.
179
+ const reachedIds = new Set();
180
+ const reachedUrls = new Set();
181
+ const byDir = new Map(actuals.map((actual) => [actual.dir, actual]));
182
+ const visited = new Set();
183
+ const stripPage = (key) => key.replace(/\.mdx?$/, '');
184
+ const pagesMetaFor = async (dir) => {
185
+ const known = byDir.get(dir);
186
+ if (known)
187
+ return known;
188
+ let parsed;
189
+ try {
190
+ parsed = JSON.parse(await readFile(join(docsRoot, dir, 'meta.json'), 'utf8'));
191
+ }
192
+ catch {
193
+ return undefined;
194
+ }
195
+ const pages = isPlainObject(parsed) ? parsed.pages : undefined;
196
+ if (!Array.isArray(pages) || pages.some((p) => typeof p !== 'string'))
197
+ return undefined;
198
+ const pagesIndex = parsed.pagesIndex;
199
+ return {
200
+ metadata: parsed,
201
+ items: pages.map((raw) => ({
202
+ raw,
203
+ item: classify(dir, raw),
204
+ })),
205
+ ...(typeof pagesIndex === 'string' && pagesIndex.trim()
206
+ ? { pagesIndex: pagesIndex.trim() }
207
+ : {}),
208
+ };
209
+ };
210
+ /**
211
+ * The page fumadocs-core 16.10.2 `buildFolder` selects as a folder's index
212
+ * (its `indexPath`): an existing path-form pagesIndex page, else `index`
213
+ * when the generator's hasImplicitIndex rule gives the folder one.
214
+ */
215
+ const folderIndexSource = async (dir, meta) => {
216
+ // buildFolder resolves an index as a page (resolveFlattenPath(..., 'page')),
217
+ // never as a same-named folder, so keep the page identity here; the
218
+ // flattened key is only for excluding the page from rest expansion.
219
+ const page = async (key) => {
220
+ const extension = extname(key);
221
+ const candidates = PAGE_EXTENSIONS.includes(extension)
222
+ ? [key]
223
+ : PAGE_EXTENSIONS.map((candidate) => `${key}${candidate}`);
224
+ for (const candidate of candidates)
225
+ if ((await entryType(docsRoot, candidate)) === 'file')
226
+ return { id: `page:${stripPage(key)}`, flat: stripPage(key) };
227
+ return undefined;
228
+ };
229
+ if (meta?.pagesIndex !== undefined) {
230
+ const index = classify(dir, meta.pagesIndex);
231
+ return index.kind === 'path' ? page(index.key) : undefined;
232
+ }
233
+ if (!hasImplicitIndex(dir, meta?.metadata ?? null))
234
+ return undefined;
235
+ return page(posix.join(dir, 'index'));
236
+ };
237
+ const visit = async (dir) => {
238
+ if (visited.has(dir))
239
+ return;
240
+ visited.add(dir);
241
+ reachedIds.add(`folder:${dir}`);
242
+ const meta = await pagesMetaFor(dir);
243
+ const excludedKeys = new Set((meta?.items ?? []).flatMap(({ item }) => item.kind === 'exclude' ? [stripPage(item.key)] : []));
244
+ const excluded = (key) => excludedKeys.has(stripPage(key));
245
+ // Folder index: a path-form pagesIndex attaches that page unless pages
246
+ // excludes it; a link-form one routes the folder to its URL (compared
247
+ // like any link item). Without pagesIndex, only a folder the generator
248
+ // also treats as non-root (hasImplicitIndex: not the docs root, no `root`)
249
+ // attaches index. The docs root's own index never enters the sidebar
250
+ // tree (the loader drops the root folder's index), so the root reaches
251
+ // its landing page only through pages, a rest entry or a link.
252
+ // The page buildFolder selects as the folder index is reserved: rest
253
+ // entries and folder listings never repeat it, even at the docs root,
254
+ // where the loader then drops the index with the root folder node.
255
+ const indexSource = await folderIndexSource(dir, meta);
256
+ if (meta?.pagesIndex !== undefined) {
257
+ const index = classify(dir, meta.pagesIndex);
258
+ if (dir !== '.' && index.kind === 'link' && index.url)
259
+ reachedUrls.add(index.url);
260
+ }
261
+ if (dir !== '.' && indexSource !== undefined && !excluded(indexSource.flat))
262
+ reachedIds.add(indexSource.id);
263
+ const reachAll = async () => {
264
+ let entries;
265
+ try {
266
+ entries = await readdir(join(docsRoot, dir), { withFileTypes: true });
267
+ }
268
+ catch {
269
+ return;
270
+ }
271
+ for (const entry of entries) {
272
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
273
+ continue;
274
+ const key = posix.join(dir, entry.name);
275
+ if (entry.isDirectory()) {
276
+ if (!excluded(key))
277
+ await visit(key);
278
+ }
279
+ else if (entry.isFile() &&
280
+ PAGE_EXTENSIONS.includes(extname(entry.name)) &&
281
+ !excluded(key) &&
282
+ stripPage(key) !== indexSource?.flat)
283
+ reachedIds.add(`page:${stripPage(key)}`);
284
+ }
285
+ };
286
+ if (!meta) {
287
+ await reachAll();
288
+ return;
289
+ }
290
+ for (const { item } of meta.items) {
291
+ if (item.kind === 'rest')
292
+ await reachAll();
293
+ else if (item.kind === 'link' && item.url)
294
+ reachedUrls.add(item.url);
295
+ else if (item.kind === 'extract') {
296
+ // Like buildFolder for `...x`: a folder's index and children are
297
+ // inlined; a page target extracts nothing.
298
+ if ((await resolveTarget(item.key)).folder)
299
+ await visit(item.key);
300
+ }
301
+ else if (item.kind === 'path') {
302
+ const target = await resolveTarget(item.key);
303
+ if (!target.exists)
304
+ continue;
305
+ if (target.folder)
306
+ await visit(item.key);
307
+ else
308
+ reachedIds.add(target.id);
309
+ }
310
+ }
311
+ };
312
+ await visit('.');
313
+ for (const actual of actuals) {
314
+ const expected = [];
315
+ for (const raw of actual.file.meta.pages) {
316
+ const item = classify(actual.dir, raw);
317
+ if (item.kind === 'path')
318
+ expected.push({
319
+ name: posix.relative(actual.dir, item.key) || item.key,
320
+ target: await resolveTarget(item.key),
321
+ });
322
+ }
323
+ const listed = [];
324
+ const excluded = new Set();
325
+ let rest = false;
326
+ for (const { item } of actual.items) {
327
+ if (item.kind === 'rest')
328
+ rest = true;
329
+ else if (item.kind === 'exclude')
330
+ excluded.add((await resolveTarget(item.key)).id);
331
+ else if (item.kind === 'path')
332
+ listed.push((await resolveTarget(item.key)).id);
333
+ else if (item.kind === 'extract') {
334
+ const target = await resolveTarget(item.key);
335
+ if (target.folder)
336
+ listed.push(target.id);
337
+ }
338
+ }
339
+ const pagesLine = lineOf(actual.source, '"pages"');
340
+ // A rest entry never shows the folder's reserved index page.
341
+ const indexSource = await folderIndexSource(actual.dir, actual);
342
+ const reservedId = indexSource?.id;
343
+ // A nested folder's landing page is implicit (never in generated pages),
344
+ // so only an explicit exclusion can hide it.
345
+ const landing = await resolveTarget(posix.join(actual.dir, 'index'));
346
+ if (actual.dir !== '.' &&
347
+ excluded.has(landing.id) &&
348
+ !expected.some((entry) => entry.target.id === landing.id))
349
+ expected.unshift({ name: 'index', target: landing });
350
+ for (const { name, target } of expected) {
351
+ if ((listed.includes(target.id) && !excluded.has(target.id)) ||
352
+ (rest && !excluded.has(target.id) && target.id !== reservedId) ||
353
+ reachedIds.has(target.id) ||
354
+ (!target.folder && reachedUrls.has(target.url)))
355
+ continue;
356
+ issues.push({
357
+ path: actual.file.path,
358
+ kind: 'missing',
359
+ message: `Missing reader ${target.folder ? 'folder' : 'page'}: "${name}" is listed in index.md Contents but no meta.json pages entry reaches it; add it to pages or remove it from Contents.`,
360
+ line: pagesLine,
361
+ });
362
+ }
363
+ const positions = [];
364
+ for (const id of listed) {
365
+ const position = expected.findIndex((entry) => entry.target.id === id);
366
+ if (position >= 0 && !positions.includes(position))
367
+ positions.push(position);
368
+ }
369
+ if (positions.some((position, index) => position < positions[index - 1]))
370
+ issues.push({
371
+ path: actual.file.path,
372
+ kind: 'order',
373
+ message: `Order differs from authored Contents: expected ${[
374
+ ...positions,
375
+ ]
376
+ .sort((a, b) => a - b)
377
+ .map((position) => expected[position].name)
378
+ .join(', ')}; found ${positions
379
+ .map((position) => expected[position].name)
380
+ .join(', ')}. Reorder pages or Contents (separators, labels and links are not compared).`,
381
+ line: pagesLine,
382
+ });
383
+ }
384
+ return issues;
385
+ }
386
+ function flattenNav(tree, out = []) {
387
+ if (!Array.isArray(tree))
388
+ return out;
389
+ for (const item of tree) {
390
+ if (typeof item === 'string')
391
+ out.push(item);
392
+ else if (isPlainObject(item))
393
+ for (const value of Object.values(item)) {
394
+ if (typeof value === 'string')
395
+ out.push(value);
396
+ else
397
+ flattenNav(value, out);
398
+ }
399
+ }
400
+ return out;
401
+ }
402
+ /**
403
+ * Compare a hand-authored MkDocs `nav:` with the Contents maps: every
404
+ * Contents page appears, every local nav page exists, and the entries of
405
+ * each Contents map (pages and section landing pages) keep their order. Section labels, nesting and external
406
+ * links are the author's. Returns no issues when `nav` is absent.
407
+ */
408
+ export async function compareMkDocsMembership(options) {
409
+ const source = await readFile(join(options.appRoot, 'mkdocs.yml'), 'utf8');
410
+ const config = YAML.parse(source);
411
+ if (!isPlainObject(config) || !Object.hasOwn(config, 'nav'))
412
+ return [];
413
+ const navLine = lineOf(source, 'nav:');
414
+ if (!Array.isArray(config.nav))
415
+ return [
416
+ {
417
+ path: 'mkdocs.yml',
418
+ kind: 'malformed',
419
+ message: 'mkdocs.yml nav must be a list; fix it or remove nav to use the default page order.',
420
+ line: navLine,
421
+ },
422
+ ];
423
+ // A page two Contents maps list appears twice in generated nav; compare
424
+ // first occurrences on both sides.
425
+ const expected = flattenNav(await buildDocsNavTree({
426
+ docsRoot: options.docsRoot,
427
+ nonReaderPaths: options.nonReaderPaths,
428
+ })).filter((path, index, all) => all.indexOf(path) === index);
429
+ const actual = [];
430
+ for (const entry of flattenNav(config.nav)) {
431
+ if (URL_SCHEME.test(entry) ||
432
+ entry.startsWith('/') ||
433
+ entry.startsWith('#'))
434
+ continue;
435
+ const path = posix.normalize(entry);
436
+ if (!actual.includes(path))
437
+ actual.push(path);
438
+ }
439
+ const issues = [];
440
+ for (const path of expected)
441
+ if (!actual.includes(path))
442
+ issues.push({
443
+ path: 'mkdocs.yml',
444
+ kind: 'missing',
445
+ message: `Missing reader page: "${path}" is listed in index.md Contents but not in mkdocs.yml nav; add it to nav or remove it from Contents.`,
446
+ line: navLine,
447
+ });
448
+ for (const path of actual)
449
+ if ((await entryType(options.docsRoot, path)) !== 'file')
450
+ issues.push({
451
+ path: 'mkdocs.yml',
452
+ kind: 'dangling',
453
+ message: `Missing target: nav entry "${path}" names no page; remove it or restore the page.`,
454
+ line: lineOf(source, path) ?? navLine,
455
+ });
456
+ // A section landing page belongs to its parent's Contents map, so whole
457
+ // sections keep the parent order; other pages group by their directory.
458
+ const owner = (path) => /^index\.mdx?$/.test(posix.basename(path)) && posix.dirname(path) !== '.'
459
+ ? posix.dirname(posix.dirname(path))
460
+ : posix.dirname(path);
461
+ const directories = [...new Set(expected.map(owner))];
462
+ for (const directory of directories) {
463
+ const want = expected.filter((path) => owner(path) === directory && actual.includes(path));
464
+ const found = actual.filter((path) => want.includes(path));
465
+ if (want.join('\n') !== found.join('\n'))
466
+ issues.push({
467
+ path: 'mkdocs.yml',
468
+ kind: 'order',
469
+ message: `Order differs from authored Contents in ${directory === '.' ? 'the docs root' : `${directory}/`}: expected ${want.join(', ')}; found ${found.join(', ')}. Reorder nav or Contents (section labels and links are not compared).`,
470
+ line: navLine,
471
+ });
472
+ }
473
+ return issues;
474
+ }
@@ -2,6 +2,7 @@ import { type CommandContext, type GlobalOptions } from '../../../app/command-co
2
2
  import { Command } from 'commander';
3
3
  import { type DocsNavTree } from './contents.js';
4
4
  import { type FumadocsMetaFile } from './fumadocs.js';
5
+ import { type NavMembershipIssue } from './membership.js';
5
6
  export type DocsNavFramework = 'fumadocs' | 'mkdocs';
6
7
  interface SyncDocsNavigationOptions {
7
8
  appRoot: string;
@@ -33,11 +34,28 @@ interface FumadocsNavigationResult {
33
34
  unlisted: string[];
34
35
  }
35
36
  type SyncDocsNavigationResult = MkDocsNavigationResult | FumadocsNavigationResult;
37
+ /** The repository's `documentation` block, when it names a root. */
38
+ interface ConfiguredDocumentation {
39
+ /** Absolute configured app or content root. */
40
+ root: string;
41
+ /** Repository-relative root as configured. */
42
+ configuredRoot: string;
43
+ tooling: string | null;
44
+ navOwner?: 'nav-sync' | 'hand-authored';
45
+ nonReaderPaths: string[];
46
+ }
47
+ interface HandAuthoredNavigationResult {
48
+ framework: DocsNavFramework;
49
+ appRoot: string;
50
+ docsRoot: string;
51
+ issues: NavMembershipIssue[];
52
+ unlisted: string[];
53
+ }
36
54
  interface DocsNavSyncDependencies {
37
55
  buildCommandContext: (options: GlobalOptions) => CommandContext;
38
- resolveConfiguredTooling: (cwd: string, appRoot: string) => Promise<string | null>;
39
- resolveNonReaderPaths?: (cwd: string, appRoot: string) => Promise<string[]>;
56
+ resolveConfiguredDocumentation: (cwd: string) => Promise<ConfiguredDocumentation | null>;
40
57
  syncDocsNavigation: (options: SyncDocsNavigationOptions) => Promise<SyncDocsNavigationResult>;
58
+ checkHandAuthoredNavigation: (options: SyncDocsNavigationOptions) => Promise<HandAuthoredNavigationResult>;
41
59
  }
42
60
  /**
43
61
  * Detect the docs framework from the app's own files: `mkdocs.yml` keeps the
@@ -46,6 +64,12 @@ interface DocsNavSyncDependencies {
46
64
  * marker, a configured `documentation.tooling` decides.
47
65
  */
48
66
  export declare function detectDocsNavFramework(appRoot: string, configuredTooling?: string | null): Promise<DocsNavFramework>;
67
+ /**
68
+ * Read-only comparison for `navOwner: hand-authored`: the same
69
+ * membership-and-order semantics as the `oat docs check` navigation rule,
70
+ * never strict generator output.
71
+ */
72
+ export declare function checkHandAuthoredNavigation(options: SyncDocsNavigationOptions): Promise<HandAuthoredNavigationResult>;
49
73
  export declare function syncDocsNavigation(options: SyncDocsNavigationOptions): Promise<SyncDocsNavigationResult>;
50
74
  export declare function createDocsNavSyncCommand(overrides?: Partial<DocsNavSyncDependencies>): Command;
51
75
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/sync.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAG5C,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAOrD,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,UAAU,sBAAsB;IAC9B,SAAS,EAAE,QAAQ,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC;IACjB,uEAAuE;IACvE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,UAAU,wBAAwB;IAChC,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,KAAK,wBAAwB,GACzB,sBAAsB,GACtB,wBAAwB,CAAC;AAE7B,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,wBAAwB,EAAE,CACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,kBAAkB,EAAE,CAClB,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,wBAAwB,CAAC,CAAC;CACxC;AAsBD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAuED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA8CnC;AAuJD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CA4BT"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/sync.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAG5C,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAOrD,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,UAAU,sBAAsB;IAC9B,SAAS,EAAE,QAAQ,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC;IACjB,uEAAuE;IACvE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,UAAU,wBAAwB;IAChC,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,KAAK,wBAAwB,GACzB,sBAAsB,GACtB,wBAAwB,CAAC;AAE7B,oEAAoE;AACpE,UAAU,uBAAuB;IAC/B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,UAAU,GAAG,eAAe,CAAC;IACxC,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,UAAU,4BAA4B;IACpC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,8BAA8B,EAAE,CAC9B,GAAG,EAAE,MAAM,KACR,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;IAC7C,kBAAkB,EAAE,CAClB,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACvC,2BAA2B,EAAE,CAC3B,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,4BAA4B,CAAC,CAAC;CAC5C;AAsBD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAuBD;;;;GAIG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,4BAA4B,CAAC,CA8BvC;AAqCD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA8CnC;AA4QD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CA4BT"}