@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
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Read-only, offline comparison used by oat-docs-analyze. No package resolution or install.
3
- import { open, readFile, realpath } from 'node:fs/promises';
4
- import { dirname, isAbsolute, join, resolve } from 'node:path';
2
+ // Read-only, offline comparison used by oat-docs-analyze. It reads installed
3
+ // package manifests the way Node resolves packages (the app's node_modules,
4
+ // then each ancestor's) and never installs, fetches or executes anything.
5
+ import { access, open, readFile, realpath } from 'node:fs/promises';
6
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
7
  import { parseArgs } from 'node:util';
6
8
 
7
9
  const shellQuote = (value) =>
@@ -75,6 +77,248 @@ async function versionsForCli(executable) {
75
77
  );
76
78
  }
77
79
 
80
+ async function exists(path) {
81
+ try {
82
+ await access(path);
83
+ return true;
84
+ } catch (error) {
85
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return false;
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ async function readJson(path) {
91
+ try {
92
+ return JSON.parse(await readFile(path, 'utf8'));
93
+ } catch (error) {
94
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null;
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ function ancestors(directory) {
100
+ const list = [];
101
+ for (let current = directory; ; current = dirname(current)) {
102
+ list.push(current);
103
+ if (dirname(current) === current) return list;
104
+ }
105
+ }
106
+
107
+ // Node package lookup: <dir>/node_modules/<name> from the app upward, so
108
+ // hoisted installs (pnpm node-linker=hoisted, npm/yarn workspaces) resolve.
109
+ async function installedManifest(appPath, name) {
110
+ for (const directory of ancestors(appPath)) {
111
+ const path = join(directory, 'node_modules', name, 'package.json');
112
+ const manifest = await readJson(path);
113
+ if (manifest !== null) return { path, manifest };
114
+ }
115
+ return null;
116
+ }
117
+
118
+ // The app's own packageManager wins, then the nearest ancestor's.
119
+ async function packageManagerFor(appPath) {
120
+ for (const directory of ancestors(appPath)) {
121
+ const manifest = await readJson(join(directory, 'package.json'));
122
+ if (typeof manifest?.packageManager === 'string')
123
+ return manifest.packageManager.trim();
124
+ }
125
+ return null;
126
+ }
127
+
128
+ // Remove a YAML comment: `#` at line start or after whitespace, outside quotes.
129
+ function stripYamlComment(line) {
130
+ let quote = null;
131
+ for (let index = 0; index < line.length; index += 1) {
132
+ const char = line[index];
133
+ if (quote) {
134
+ if (char === quote) quote = null;
135
+ } else if (char === '"' || char === "'") quote = char;
136
+ else if (char === '#' && (index === 0 || /\s/.test(line[index - 1])))
137
+ return line.slice(0, index);
138
+ }
139
+ return line;
140
+ }
141
+
142
+ function unquoteYaml(value) {
143
+ const text = value.trim();
144
+ if (text.length >= 2 && text.startsWith("'") && text.endsWith("'"))
145
+ return text.slice(1, -1).replaceAll("''", "'");
146
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
147
+ try {
148
+ return JSON.parse(text);
149
+ } catch {
150
+ return text.slice(1, -1);
151
+ }
152
+ }
153
+ return text;
154
+ }
155
+
156
+ // Split a flow sequence body on commas outside quotes and braces.
157
+ function splitFlowItems(body) {
158
+ const items = [];
159
+ let quote = null;
160
+ let depth = 0;
161
+ let current = '';
162
+ for (const char of body) {
163
+ if (quote) {
164
+ if (char === quote) quote = null;
165
+ } else if (char === '"' || char === "'") quote = char;
166
+ else if (char === '{') depth += 1;
167
+ else if (char === '}') depth -= 1;
168
+ else if (char === ',' && depth === 0) {
169
+ items.push(current);
170
+ current = '';
171
+ continue;
172
+ }
173
+ current += char;
174
+ }
175
+ items.push(current);
176
+ return items;
177
+ }
178
+
179
+ // The `packages` list of a pnpm-workspace.yaml: an indented or unindented
180
+ // block sequence or a flow sequence, with quotes and comments. A file
181
+ // without `packages` (settings only) declares no member packages.
182
+ function workspacePatterns(text) {
183
+ const lines = text.split(/\r?\n/).map(stripYamlComment);
184
+ const start = lines.findIndex((line) => /^packages\s*:/.test(line));
185
+ if (start < 0) return [];
186
+ const inline = lines[start].replace(/^packages\s*:/, '').trim();
187
+ if (inline.startsWith('[')) {
188
+ let flow = inline;
189
+ for (
190
+ let index = start + 1;
191
+ !flow.includes(']') && index < lines.length;
192
+ index += 1
193
+ )
194
+ flow += ` ${lines[index].trim()}`;
195
+ return splitFlowItems(flow.slice(1, flow.lastIndexOf(']')))
196
+ .map(unquoteYaml)
197
+ .filter((entry) => entry.length > 0);
198
+ }
199
+ const patterns = [];
200
+ for (const line of lines.slice(start + 1)) {
201
+ if (line.trim() === '') continue;
202
+ const item = line.match(/^\s*-\s*(.*)$/);
203
+ if (!item) break;
204
+ patterns.push(unquoteYaml(item[1]));
205
+ }
206
+ return patterns.filter((entry) => entry.length > 0);
207
+ }
208
+
209
+ // Expand `{a,b}` alternatives, innermost first.
210
+ function expandBraces(pattern) {
211
+ const match = pattern.match(/\{([^{}]*)\}/);
212
+ if (!match) return [pattern];
213
+ return match[1]
214
+ .split(',')
215
+ .flatMap((alternative) =>
216
+ expandBraces(
217
+ pattern.slice(0, match.index) +
218
+ alternative +
219
+ pattern.slice(match.index + match[0].length),
220
+ ),
221
+ );
222
+ }
223
+
224
+ // Glob syntax evaluated here: `*`, `**`, `?`, braces (expanded first) and
225
+ // positive character classes. Extglobs, negated classes, escapes and
226
+ // unbalanced brackets are not, and never count as a non-match.
227
+ function unsupportedGlob(pattern) {
228
+ return (
229
+ /[+@!?*]\(/.test(pattern) ||
230
+ /\[[!^]/.test(pattern) ||
231
+ pattern.includes('\\') ||
232
+ /\[(?![^\]]*\])/.test(pattern) ||
233
+ /\[\]/.test(pattern)
234
+ );
235
+ }
236
+
237
+ function globToRegExp(pattern) {
238
+ const source = pattern
239
+ .split(/(\*\*\/|\*\*|\*|\?|\[[^\]]+\])/)
240
+ .map((part) =>
241
+ part === '**/'
242
+ ? '(?:.*/)?'
243
+ : part === '**'
244
+ ? '.*'
245
+ : part === '*'
246
+ ? '[^/]*'
247
+ : part === '?'
248
+ ? '[^/]'
249
+ : part.startsWith('[') && part.endsWith(']')
250
+ ? `(?!/)[${part.slice(1, -1).replace(/[\\\]]/g, '\\$&')}]`
251
+ : part.replace(/[.+^${}()|[\]\\]/g, '\\$&'),
252
+ )
253
+ .join('');
254
+ return new RegExp(`^${source}$`);
255
+ }
256
+
257
+ // pnpm finds members by globbing `<pattern>/package.json`, so a trailing
258
+ // globstar also matches the directory itself; negations exclude. Returns
259
+ // true or false, or `{ unsupported: pattern }` when a pattern whose result
260
+ // matters cannot be evaluated.
261
+ function workspaceIncludes(patterns, rel) {
262
+ const target = `${rel}/package.json`;
263
+ const evaluate = (pattern) => {
264
+ const expanded = expandBraces(
265
+ pattern.replace(/^\.\//, '').replace(/\/+$/, ''),
266
+ );
267
+ if (expanded.some(unsupportedGlob)) return { unsupported: pattern };
268
+ return expanded.some((glob) =>
269
+ globToRegExp(`${glob}/package.json`).test(target),
270
+ );
271
+ };
272
+ const any = (list) => {
273
+ let unknown = null;
274
+ for (const pattern of list) {
275
+ const result = evaluate(pattern);
276
+ if (result === true) return true;
277
+ if (result !== false && unknown === null) unknown = result;
278
+ }
279
+ return unknown ?? false;
280
+ };
281
+ const included = any(patterns.filter((pattern) => !pattern.startsWith('!')));
282
+ if (included !== true) return included;
283
+ const excluded = any(
284
+ patterns
285
+ .filter((pattern) => pattern.startsWith('!'))
286
+ .map((pattern) => pattern.slice(1)),
287
+ );
288
+ return excluded === true ? false : excluded === false ? true : excluded;
289
+ }
290
+
291
+ // The app's relation to the nearest pnpm workspace: its own root, a member of
292
+ // an ancestor workspace, standalone (the ancestor's `packages` globs exclude
293
+ // it, so installs need --ignore-workspace), or undetermined when a pattern
294
+ // cannot be evaluated. Lockfile presence is not evidence: a member can keep
295
+ // its own lockfile with shared-workspace-lockfile=false.
296
+ async function workspaceMembership(appPath) {
297
+ if (await exists(join(appPath, 'pnpm-workspace.yaml')))
298
+ return { root: appPath, membership: 'root', reason: null };
299
+ for (const directory of ancestors(dirname(appPath))) {
300
+ const file = join(directory, 'pnpm-workspace.yaml');
301
+ if (!(await exists(file))) continue;
302
+ const rel = relative(directory, appPath).split(sep).join('/');
303
+ const included = workspaceIncludes(
304
+ workspacePatterns(await readFile(file, 'utf8')),
305
+ rel,
306
+ );
307
+ if (typeof included === 'boolean')
308
+ return {
309
+ root: directory,
310
+ membership: included ? 'member' : 'standalone',
311
+ reason: null,
312
+ };
313
+ return {
314
+ root: directory,
315
+ membership: 'undetermined',
316
+ reason: `pnpm-workspace.yaml pattern "${included.unsupported}" uses glob syntax this helper does not evaluate; workspace membership is unknown, so the command omits --ignore-workspace. Confirm membership with pnpm -r ls before running it.`,
317
+ };
318
+ }
319
+ return { root: null, membership: null, reason: null };
320
+ }
321
+
78
322
  try {
79
323
  const { values } = parseArgs({
80
324
  options: {
@@ -84,14 +328,46 @@ try {
84
328
  },
85
329
  });
86
330
  if (!values.app) throw new Error('Required: --app <docs-app-directory>.');
331
+ const appPath = resolve(values.app);
332
+ const manifest = await readJson(join(appPath, 'package.json'));
333
+ if (manifest === null) {
334
+ process.stdout.write(
335
+ `${JSON.stringify(
336
+ {
337
+ versionsPath: null,
338
+ appPath,
339
+ applicable: false,
340
+ reason:
341
+ 'No package.json in the docs directory: Markdown docs have no app-package drift check.',
342
+ packages: [],
343
+ },
344
+ null,
345
+ 2,
346
+ )}\n`,
347
+ );
348
+ process.exit(0);
349
+ }
87
350
  const versionsPath = resolve(
88
351
  values.versions ?? (await versionsForCli(values.cli)),
89
352
  );
90
353
  const versions = JSON.parse(await readFile(versionsPath, 'utf8'));
91
- const appPath = resolve(values.app);
92
- const manifest = JSON.parse(
93
- await readFile(join(appPath, 'package.json'), 'utf8'),
354
+ const packageManager = await packageManagerFor(appPath);
355
+ const pinned = packageManager?.match(
356
+ /^pnpm@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/,
94
357
  );
358
+ // Run the repository's pinned pnpm rather than whatever global pnpm is on
359
+ // PATH: a different major can rewrite workspace settings or reject the store.
360
+ // npx ships with npm on every Node line; Corepack is absent from Node 25+.
361
+ const pnpm = pinned ? `npx --yes pnpm@${pinned[1]}` : 'pnpm';
362
+ const otherManager =
363
+ packageManager && !packageManager.startsWith('pnpm@')
364
+ ? packageManager
365
+ : null;
366
+ // A nested app the ancestor workspace does not include must be installed
367
+ // with --ignore-workspace; an undetermined result never adds the flag.
368
+ const workspace = await workspaceMembership(appPath);
369
+ const ignoreWorkspace =
370
+ workspace.membership === 'standalone' ? ' --ignore-workspace' : '';
95
371
  const packages = [];
96
372
  for (const section of [
97
373
  'dependencies',
@@ -116,29 +392,22 @@ try {
116
392
  )
117
393
  throw new Error(`Missing/invalid bundled version for ${name}.`);
118
394
  // Report installed package bytes when available, not just a declared range.
119
- let installed = null;
120
- try {
121
- const installedManifest = JSON.parse(
122
- await readFile(
123
- join(appPath, 'node_modules', name, 'package.json'),
124
- 'utf8',
125
- ),
126
- );
127
- if (typeof installedManifest.version !== 'string')
128
- throw new Error(`Invalid installed version for ${name}.`);
129
- installed = installedManifest.version;
130
- } catch (error) {
131
- if (error.code !== 'ENOENT') throw error;
132
- }
395
+ const found = await installedManifest(appPath, name);
396
+ if (found !== null && typeof found.manifest.version !== 'string')
397
+ throw new Error(`Invalid installed version for ${name}.`);
398
+ const installed = found?.manifest.version ?? null;
133
399
  // Only stable numeric releases are orderable here; prereleases and other
134
400
  // package-manager version forms need manual verification, never an upgrade.
135
401
  const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
136
402
  let status = 'unverified';
137
- if (
138
- installed !== null &&
139
- stable.test(installed) &&
140
- stable.test(bundled)
141
- ) {
403
+ let reason = null;
404
+ if (installed === null)
405
+ reason = `not installed: no node_modules/${name} found from the app or its ancestors; run the app install first`;
406
+ else if (!stable.test(installed))
407
+ reason = `installed version ${installed} is not a stable release; verify it manually`;
408
+ else if (!stable.test(bundled))
409
+ reason = `bundled version ${bundled} is a prerelease; verify it manually`;
410
+ else {
142
411
  const left = installed.split('.').map(BigInt);
143
412
  const right = bundled.split('.').map(BigInt);
144
413
  const differing = left.findIndex(
@@ -151,22 +420,26 @@ try {
151
420
  ? 'drift'
152
421
  : 'ahead';
153
422
  }
423
+ let upgradeCommand = null;
424
+ if (status === 'drift' && otherManager)
425
+ reason = `packageManager is ${otherManager}; translate the upgrade to its add command, preserving the dependency section and exact version`;
426
+ else if (status === 'drift')
427
+ upgradeCommand = `${pnpm} --dir ${shellQuote(appPath)} add${ignoreWorkspace}${section === 'devDependencies' ? ' --save-dev' : section === 'optionalDependencies' ? ' --save-optional' : ''} ${name}@${bundled}`;
154
428
  packages.push({
155
429
  name,
156
430
  section,
157
431
  declared,
158
432
  installed,
433
+ installedPath: found?.path ?? null,
159
434
  bundled,
160
435
  status,
161
- upgradeCommand:
162
- status === 'drift' || status === 'current'
163
- ? `pnpm --dir ${shellQuote(appPath)} add${section === 'devDependencies' ? ' --save-dev' : section === 'optionalDependencies' ? ' --save-optional' : ''} ${name}@${bundled}`
164
- : null,
436
+ reason,
437
+ upgradeCommand,
165
438
  });
166
439
  }
167
440
  }
168
441
  process.stdout.write(
169
- `${JSON.stringify({ versionsPath, appPath, packages }, null, 2)}\n`,
442
+ `${JSON.stringify({ versionsPath, appPath, applicable: true, packageManager, workspace, packages }, null, 2)}\n`,
170
443
  );
171
444
  } catch (error) {
172
445
  process.stderr.write(`${error.message}\n`);
@@ -5,7 +5,7 @@ disable-model-invocation: true
5
5
  user-invocable: true
6
6
  allowed-tools: Read, Write, Edit, Bash(git:*), Bash(gh:*), Bash(oat:*), Bash(pnpm:*), Bash(bash:*), Bash(jq:*), Bash(mkdir:*), Bash(date:*), Bash(echo:*), Bash(cd:*), Bash(pwd:*), Glob, Grep, AskUserQuestion
7
7
  metadata:
8
- version: '1.4.7'
8
+ version: '1.4.8'
9
9
  ---
10
10
 
11
11
  # Docs Apply
@@ -324,6 +324,8 @@ approval, its out-of-Markdown allowance is only these four operations:
324
324
  app's own `@open-agent-toolkit/cli` dependency when the app declares it, to the
325
325
  bundled versions using the artifact's exact upgrade commands (preserve each
326
326
  dependency section and the package manager; include the resulting lockfile only).
327
+ Run the pinned package-manager form the command gives; a global pnpm of another
328
+ major can rewrite workspace settings and break the store link.
327
329
  Never downgrade an installed version: `ahead` or `unverified` requires CLI
328
330
  selection/update or manual verification before re-analysis, not a tooling update.
329
331
  2. After a CLI bump, regenerate the app's generated manifest (`index.md`) with that
@@ -6,7 +6,7 @@ disable-model-invocation: false
6
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
7
7
  user-invocable: true
8
8
  metadata:
9
- version: '1.2.1'
9
+ version: '1.2.2'
10
10
  ---
11
11
 
12
12
  # OAT Docs Authoring
@@ -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":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/config/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAIhF,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,iCAAiC,CAAC;AAsBzC,OAAO,EAUL,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,cAAc,EAQnB,KAAK,UAAU,EAwBhB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAGL,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAGL,KAAK,8BAA8B,EACnC,KAAK,yBAAyB,EAC/B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,mCAAmC,EACnC,0BAA0B,EAC3B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8KpC,UAAU,yBAAyB;IACjC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,kCAAkC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IAC7E,2CAA2C,EAAE,CAC3C,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,SAAS,CAAC,CAAC;IACxB,gDAAgD,EAAE,CAChD,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,SAAS,CAAC,CAAC;IACxB,6CAA6C,EAAE,CAC7C,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,SAAS,CAAC,CAAC;IACxB,yBAAyB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACxE,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,iBAAiB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzE,kBAAkB,EAAE,CAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAC7C,mCAAmC,EAAE,OAAO,mCAAmC,CAAC;IAChF,0BAA0B,EAAE,OAAO,0BAA0B,CAAC;IAC9D,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAk6HD,wBAAgB,mBAAmB,CACjC,SAAS,GAAE,OAAO,CAAC,yBAAyB,CAAM,GACjD,OAAO,CAqMT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/config/index.ts"],"names":[],"mappings":"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"}
@@ -77,6 +77,7 @@ const KEY_ORDER = [
77
77
  'documentation.root',
78
78
  'documentation.tooling',
79
79
  'documentation.config',
80
+ 'documentation.index',
80
81
  'documentation.excludes',
81
82
  'documentation.requireForProjectCompletion',
82
83
  'explainers.defaults.style',
@@ -322,6 +323,17 @@ const CONFIG_CATALOG = [
322
323
  owningCommand: 'oat config set documentation.config <value>',
323
324
  description: 'Repository-relative path to the primary documentation tool config file.',
324
325
  },
326
+ {
327
+ key: 'documentation.index',
328
+ group: 'Shared Repo (.oat/config.json)',
329
+ file: '.oat/config.json',
330
+ scope: 'shared repo',
331
+ type: 'string',
332
+ defaultValue: 'unset',
333
+ mutability: 'read/write',
334
+ owningCommand: 'oat config set documentation.index <value>',
335
+ description: 'Repository-relative docs index path. Markdown: the authored root index.md. Fumadocs: the generated app-root manifest; MkDocs: mkdocs.yml. oat docs init writes it and oat docs generate-index updates the Fumadocs transition.',
336
+ },
325
337
  {
326
338
  key: 'documentation.nonReaderPaths',
327
339
  group: 'Shared Repo (.oat/config.json)',
@@ -330,7 +342,7 @@ const CONFIG_CATALOG = [
330
342
  type: 'string[]',
331
343
  defaultValue: 'unset',
332
344
  mutability: 'read/write',
333
- owningCommand: 'oat config set documentation.nonReaderPaths <glob[,glob...]>',
345
+ owningCommand: 'oat config set documentation.nonReaderPaths <glob[,glob...]|JSON-array>',
334
346
  description: 'Content-root-relative globs excluded from reader navigation, manifest and supported framework rendering. Empty clears the key.',
335
347
  },
336
348
  {
@@ -341,7 +353,7 @@ const CONFIG_CATALOG = [
341
353
  type: 'string[]',
342
354
  defaultValue: 'unset',
343
355
  mutability: 'read/write',
344
- owningCommand: 'oat config set documentation.excludes <glob[,glob...]>',
356
+ owningCommand: 'oat config set documentation.excludes <glob[,glob...]|JSON-array>',
345
357
  description: 'Comma-separated globs, relative to the docs directory, excluded from `oat docs generate-index`. Repeated `--exclude` flags extend this list; an empty value clears the key.',
346
358
  },
347
359
  {
@@ -1113,7 +1125,24 @@ function parseInstructionPointerExcludes(rawValue) {
1113
1125
  }
1114
1126
  return normalized;
1115
1127
  }
1116
- function parseDocumentationExcludes(rawValue) {
1128
+ function parseDocumentationExcludes(rawValue, key) {
1129
+ const trimmed = rawValue.trim();
1130
+ // A glob list may be given as a JSON array (which also keeps braces such as
1131
+ // `{a,b}` intact). Anything else that looks like JSON is a mistake, never a
1132
+ // literal glob.
1133
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
1134
+ let parsed;
1135
+ try {
1136
+ parsed = JSON.parse(trimmed);
1137
+ }
1138
+ catch {
1139
+ parsed = undefined;
1140
+ }
1141
+ if (!Array.isArray(parsed) ||
1142
+ parsed.some((entry) => typeof entry !== 'string' || entry.trim().length === 0))
1143
+ throw new Error(`Invalid ${key} value: a value starting with [ or { must be a JSON array of non-empty strings; otherwise use comma-separated globs.`);
1144
+ return [...new Set(parsed.map((entry) => entry.trim()))];
1145
+ }
1117
1146
  const parsed = rawValue
1118
1147
  .split(',')
1119
1148
  .map((entry) => entry.trim())
@@ -1853,7 +1882,7 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
1853
1882
  value = normalizeDocumentationCheck(parsed);
1854
1883
  }
1855
1884
  else if (key === 'documentation.nonReaderPaths') {
1856
- const paths = parseDocumentationExcludes(rawValue);
1885
+ const paths = parseDocumentationExcludes(rawValue, 'documentation.nonReaderPaths');
1857
1886
  value = paths.length > 0 ? paths : undefined;
1858
1887
  }
1859
1888
  else if (key === 'documentation.ia') {
@@ -1870,7 +1899,8 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
1870
1899
  }
1871
1900
  }
1872
1901
  catch (error) {
1873
- if (key === 'documentation.check')
1902
+ if (key === 'documentation.check' ||
1903
+ key === 'documentation.nonReaderPaths')
1874
1904
  throw error;
1875
1905
  if (error instanceof DocumentationIaPresetError)
1876
1906
  throw new Error(`Invalid ${key} value: ${error.detail}.`, {
@@ -2106,8 +2136,11 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
2106
2136
  else if (key === 'documentation.config') {
2107
2137
  doc.config = normalizeSharedRoot(rawValue);
2108
2138
  }
2139
+ else if (key === 'documentation.index') {
2140
+ doc.index = normalizeSharedRoot(rawValue);
2141
+ }
2109
2142
  else if (key === 'documentation.excludes') {
2110
- const excludes = parseDocumentationExcludes(rawValue);
2143
+ const excludes = parseDocumentationExcludes(rawValue, 'documentation.excludes');
2111
2144
  if (excludes.length === 0) {
2112
2145
  // Unlike the scalar shared keys, an empty value is the documented way
2113
2146
  // to clear this list rather than an error.
@@ -2265,9 +2298,9 @@ function withoutOwnKey(record, key) {
2265
2298
  *
2266
2299
  * Pruning is strictly "the object has no remaining own keys". That is what
2267
2300
  * keeps a prune from destroying a sibling the `oat config` catalog does not
2268
- * expose — `documentation.index` is parsed by `config/oat-config.ts` but is
2269
- * not a `ConfigKey`, so an unset of the last catalogued `documentation.*` key
2270
- * leaves the parent in place while it is still present.
2301
+ * expose (such as a key a newer CLI wrote): an unset of the last catalogued
2302
+ * `documentation.*` key leaves the parent in place while any other key is
2303
+ * still present.
2271
2304
  *
2272
2305
  * A missing path, a `null`/`undefined` leaf, or a non-object in the middle of
2273
2306
  * the path all report `removed: false`, which the caller surfaces as the
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/check.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,sBAAsB,IAAI,OAAO,CA4DhD"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/check.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,sBAAsB,IAAI,OAAO,CA6DhD"}
@@ -15,6 +15,7 @@ export function createDocsCheckCommand() {
15
15
  cwd: context.cwd,
16
16
  targetDir: options.targetDir,
17
17
  rules: options.rule,
18
+ onNotice: (message) => context.logger.warn(message),
18
19
  });
19
20
  process.exitCode = result.summary.errors ? 1 : 0;
20
21
  }
@@ -4,5 +4,7 @@ export declare function checkDocs(options: {
4
4
  cwd: string;
5
5
  targetDir?: string;
6
6
  rules?: string[];
7
+ /** Receives human notices; they never enter the JSON result. */
8
+ onNotice?: (message: string) => void;
7
9
  }): Promise<DocsCheckResult>;
8
10
  //# sourceMappingURL=checker.d.ts.map
@@ -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;CAClB,GAAG,OAAO,CAAC,eAAe,CAAC,CAwF3B"}
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"}
@@ -11,6 +11,8 @@ export async function checkDocs(options) {
11
11
  if (unknown)
12
12
  throw new Error(`Unknown docs check rule "${unknown}". Choose: ${DOCS_CHECK_RULES.join(', ')}.`);
13
13
  const target = await resolveCheckTarget(options.cwd, options.targetDir);
14
+ for (const notice of target.notices)
15
+ options.onNotice?.(notice);
14
16
  const policy = target.documentation.check;
15
17
  const findings = [];
16
18
  const exceptions = policy?.exceptions?.map((e) => ({
@@ -53,7 +55,10 @@ export async function checkDocs(options) {
53
55
  await checkReaders(target, pages, report);
54
56
  checkProvenance(target, pages, report);
55
57
  if (!options.rules?.length || options.rules.includes('navigation'))
56
- 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));
57
62
  if (!options.rules?.length || options.rules.includes('commands'))
58
63
  await checkCommands(target, pages, report);
59
64
  if (!options.rules?.length ||
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgI7C,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAyJf"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgI7C,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAkNf"}