@open-agent-toolkit/cli 0.3.27 → 0.3.29

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 (105) hide show
  1. package/README.md +8 -0
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/contributing/documentation.md +11 -2
  4. package/assets/docs/docs-tooling/add-docs-to-a-repo.md +12 -0
  5. package/assets/docs/docs-tooling/check.md +154 -0
  6. package/assets/docs/docs-tooling/commands.md +61 -11
  7. package/assets/docs/docs-tooling/index.md +1 -0
  8. package/assets/docs/docs-tooling/meta.json +7 -1
  9. package/assets/docs/docs-tooling/workflows.md +4 -0
  10. package/assets/docs/reference/cli-reference.md +2 -0
  11. package/assets/docs/reference/configuration.md +1 -0
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/authoring-docs/SKILL.md +2 -2
  14. package/assets/skills/authoring-docs/references/information-architecture.md +5 -2
  15. package/assets/skills/authoring-docs/references/page-templates/alert-runbook.md +63 -0
  16. package/assets/skills/authoring-docs/references/page-templates/command-reference.md +55 -0
  17. package/assets/skills/authoring-docs/references/page-templates/decisions-pointer.md +44 -0
  18. package/assets/skills/authoring-docs/references/page-templates/engineering-feature.md +53 -0
  19. package/assets/skills/authoring-docs/references/page-templates/per-area-agents.md +42 -0
  20. package/assets/skills/authoring-docs/references/page-templates/product-feature.md +49 -0
  21. package/assets/skills/authoring-docs/references/page-templates/qa-validation.md +59 -0
  22. package/assets/skills/authoring-docs/references/page-templates/screen-spec.md +51 -0
  23. package/assets/skills/authoring-docs/references/page-templates/skill-command-guide.md +44 -0
  24. package/assets/skills/authoring-docs/references/page-templates/solution-record.md +43 -0
  25. package/assets/skills/authoring-docs/references/templates.md +22 -2
  26. package/assets/skills/oat-docs-analyze/SKILL.md +13 -2
  27. package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
  28. package/assets/skills/oat-docs-authoring/references/validation.md +4 -0
  29. package/assets/skills/oat-docs-bootstrap/SKILL.md +63 -6
  30. package/assets/skills/oat-docs-bootstrap/references/pr-ci.md +89 -0
  31. package/assets/templates/docs-app-fuma/AGENTS.md +9 -2
  32. package/assets/templates/docs-app-fuma/docs/contributing/docs-toolchain.md +40 -3
  33. package/assets/templates/docs-app-fuma/docs/contributing.md +7 -1
  34. package/assets/templates/docs-app-fuma/docs/index.md +1 -1
  35. package/assets/templates/docs-app-fuma/gitignore.template +12 -0
  36. package/assets/templates/docs-app-fuma/package.json.template +2 -1
  37. package/assets/templates/docs-app-fuma/pnpm-workspace.yaml +5 -0
  38. package/assets/templates/docs-app-mkdocs/AGENTS.md +4 -2
  39. package/assets/templates/docs-app-mkdocs/docs/contributing/docs-toolchain.md +4 -2
  40. package/assets/templates/docs-app-mkdocs/docs/contributing.md +7 -1
  41. package/assets/templates/docs-app-mkdocs/docs/index.md +1 -1
  42. package/assets/templates/docs-app-mkdocs/gitignore.template +8 -0
  43. package/assets/templates/docs-app-mkdocs/package.json.template +3 -2
  44. package/dist/commands/config/index.d.ts.map +1 -1
  45. package/dist/commands/config/index.js +35 -3
  46. package/dist/commands/docs/check/check.d.ts +3 -0
  47. package/dist/commands/docs/check/check.d.ts.map +1 -0
  48. package/dist/commands/docs/check/check.js +48 -0
  49. package/dist/commands/docs/check/check.types.d.ts +17 -0
  50. package/dist/commands/docs/check/check.types.d.ts.map +1 -0
  51. package/dist/commands/docs/check/check.types.js +1 -0
  52. package/dist/commands/docs/check/checker.d.ts +8 -0
  53. package/dist/commands/docs/check/checker.d.ts.map +1 -0
  54. package/dist/commands/docs/check/checker.js +82 -0
  55. package/dist/commands/docs/check/commands.d.ts +5 -0
  56. package/dist/commands/docs/check/commands.d.ts.map +1 -0
  57. package/dist/commands/docs/check/commands.js +232 -0
  58. package/dist/commands/docs/check/diagrams.d.ts +5 -0
  59. package/dist/commands/docs/check/diagrams.d.ts.map +1 -0
  60. package/dist/commands/docs/check/diagrams.js +197 -0
  61. package/dist/commands/docs/check/files.d.ts +21 -0
  62. package/dist/commands/docs/check/files.d.ts.map +1 -0
  63. package/dist/commands/docs/check/files.js +121 -0
  64. package/dist/commands/docs/check/markdown.d.ts +60 -0
  65. package/dist/commands/docs/check/markdown.d.ts.map +1 -0
  66. package/dist/commands/docs/check/markdown.js +125 -0
  67. package/dist/commands/docs/check/mermaid-runtime.d.ts +3 -0
  68. package/dist/commands/docs/check/mermaid-runtime.d.ts.map +1 -0
  69. package/dist/commands/docs/check/mermaid-runtime.js +57 -0
  70. package/dist/commands/docs/check/navigation.d.ts +4 -0
  71. package/dist/commands/docs/check/navigation.d.ts.map +1 -0
  72. package/dist/commands/docs/check/navigation.js +104 -0
  73. package/dist/commands/docs/check/reader-rules.d.ts +6 -0
  74. package/dist/commands/docs/check/reader-rules.d.ts.map +1 -0
  75. package/dist/commands/docs/check/reader-rules.js +214 -0
  76. package/dist/commands/docs/index.d.ts.map +1 -1
  77. package/dist/commands/docs/index.js +2 -0
  78. package/dist/commands/docs/init/content.d.ts +23 -0
  79. package/dist/commands/docs/init/content.d.ts.map +1 -0
  80. package/dist/commands/docs/init/content.js +361 -0
  81. package/dist/commands/docs/init/index.d.ts +1 -0
  82. package/dist/commands/docs/init/index.d.ts.map +1 -1
  83. package/dist/commands/docs/init/index.js +40 -1
  84. package/dist/commands/docs/init/markdown.d.ts.map +1 -1
  85. package/dist/commands/docs/init/markdown.js +21 -2
  86. package/dist/commands/docs/init/resolve-options.d.ts +4 -1
  87. package/dist/commands/docs/init/resolve-options.d.ts.map +1 -1
  88. package/dist/commands/docs/init/resolve-options.js +4 -0
  89. package/dist/commands/docs/init/scaffold.d.ts +1 -0
  90. package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
  91. package/dist/commands/docs/init/scaffold.js +57 -4
  92. package/dist/commands/docs/nav/contents.d.ts +4 -0
  93. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  94. package/dist/commands/docs/nav/contents.js +10 -0
  95. package/dist/config/documentation-check.d.ts +38 -0
  96. package/dist/config/documentation-check.d.ts.map +1 -0
  97. package/dist/config/documentation-check.js +132 -0
  98. package/dist/config/oat-config.d.ts +2 -0
  99. package/dist/config/oat-config.d.ts.map +1 -1
  100. package/dist/config/oat-config.js +4 -0
  101. package/dist/config/resolve.d.ts.map +1 -1
  102. package/dist/config/resolve.js +1 -0
  103. package/dist/release/public-package-contract.d.ts.map +1 -1
  104. package/dist/release/public-package-contract.js +13 -0
  105. package/package.json +11 -2
@@ -0,0 +1,82 @@
1
+ import { createExclusionMatcher, isExcludedPath, } from '../../docs/shared/exclusions.js';
2
+ import { DOCS_CHECK_RULES, } from '../../../config/documentation-check.js';
3
+ import { checkCommands } from './commands.js';
4
+ import { checkDiagrams } from './diagrams.js';
5
+ import { readPage, readerFiles, resolveCheckTarget } from './files.js';
6
+ import { checkNavigation } from './navigation.js';
7
+ import { checkProvenance, checkReaders } from './reader-rules.js';
8
+ /** Read-only portable checker. Output paths are content-root-relative POSIX. */
9
+ export async function checkDocs(options) {
10
+ const unknown = options.rules?.find((rule) => !DOCS_CHECK_RULES.includes(rule));
11
+ if (unknown)
12
+ throw new Error(`Unknown docs check rule "${unknown}". Choose: ${DOCS_CHECK_RULES.join(', ')}.`);
13
+ const target = await resolveCheckTarget(options.cwd, options.targetDir);
14
+ const policy = target.documentation.check;
15
+ const findings = [];
16
+ const exceptions = policy?.exceptions?.map((e) => ({
17
+ ...e,
18
+ matcher: createExclusionMatcher([e.path]),
19
+ })) ?? [];
20
+ const report = (ruleId, defaultSeverity, path, message, line) => {
21
+ if (options.rules?.length && !options.rules.includes(ruleId))
22
+ return;
23
+ let severity = policy?.rules?.[ruleId] ?? defaultSeverity;
24
+ for (const e of exceptions)
25
+ if (e.rule === ruleId && isExcludedPath(e.matcher, path))
26
+ severity = e.severity ?? 'off';
27
+ if (severity === 'off')
28
+ return;
29
+ findings.push({
30
+ ruleId,
31
+ severity,
32
+ path,
33
+ ...(line === undefined ? {} : { line }),
34
+ message,
35
+ });
36
+ };
37
+ const pages = [];
38
+ for (const path of await readerFiles(target)) {
39
+ try {
40
+ const page = await readPage(target, path);
41
+ pages.push(page);
42
+ if (page.frontmatterError)
43
+ report('frontmatter', 'error', path, page.frontmatterError, 1);
44
+ }
45
+ catch (error) {
46
+ const code = error?.code;
47
+ const safeCode = typeof code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code)
48
+ ? code
49
+ : 'UNKNOWN';
50
+ report('frontmatter', 'error', path, `Cannot read documentation source (${safeCode}); check file permissions and readability.`, 1);
51
+ }
52
+ }
53
+ await checkReaders(target, pages, report);
54
+ checkProvenance(target, pages, report);
55
+ if (!options.rules?.length || options.rules.includes('navigation'))
56
+ await checkNavigation(target, report);
57
+ if (!options.rules?.length || options.rules.includes('commands'))
58
+ await checkCommands(target, pages, report);
59
+ if (!options.rules?.length ||
60
+ options.rules.some((r) => [
61
+ 'mermaid',
62
+ 'diagram-accessibility',
63
+ 'diagram-pairs',
64
+ 'diagram-freshness',
65
+ 'image-alt',
66
+ ].includes(r)))
67
+ await checkDiagrams(target, pages, report);
68
+ findings.sort((a, b) => a.path < b.path
69
+ ? -1
70
+ : a.path > b.path
71
+ ? 1
72
+ : (a.line ?? 0) - (b.line ?? 0) ||
73
+ a.ruleId.localeCompare(b.ruleId) ||
74
+ a.message.localeCompare(b.message));
75
+ return {
76
+ summary: {
77
+ errors: findings.filter((f) => f.severity === 'error').length,
78
+ warnings: findings.filter((f) => f.severity === 'warn').length,
79
+ },
80
+ findings,
81
+ };
82
+ }
@@ -0,0 +1,5 @@
1
+ import type { Report } from './check.types.js';
2
+ import { type CheckTarget } from './files.js';
3
+ import type { ParsedPage } from './markdown.js';
4
+ export declare function checkCommands(target: CheckTarget, pages: ParsedPage[], report: Report): Promise<void>;
5
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,232 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
+ import { createExclusionMatcher } from '../../docs/shared/exclusions.js';
4
+ import YAML from 'yaml';
5
+ import { inside, posix, present } from './files.js';
6
+ const BUILTINS = new Set([
7
+ 'add',
8
+ 'install',
9
+ 'i',
10
+ 'exec',
11
+ 'dlx',
12
+ 'remove',
13
+ 'rm',
14
+ 'uninstall',
15
+ 'update',
16
+ 'up',
17
+ 'outdated',
18
+ 'list',
19
+ 'ls',
20
+ 'why',
21
+ 'link',
22
+ 'unlink',
23
+ 'import',
24
+ 'rebuild',
25
+ 'prune',
26
+ 'fetch',
27
+ 'store',
28
+ 'config',
29
+ 'setup',
30
+ 'env',
31
+ 'help',
32
+ 'init',
33
+ 'create',
34
+ 'publish',
35
+ 'pack',
36
+ 'audit',
37
+ 'licenses',
38
+ 'patch',
39
+ 'patch-commit',
40
+ 'patch-remove',
41
+ 'deploy',
42
+ 'dedupe',
43
+ 'approve-builds',
44
+ 'ignored-builds',
45
+ 'server',
46
+ 'self-update',
47
+ 'root',
48
+ 'bin',
49
+ 'version',
50
+ ]);
51
+ const illustrative = (value) => /[<$`{}]|\.\.\./.test(value);
52
+ function tokens(source) {
53
+ return (source.match(/"(?:\\.|[^"\\])*"|'[^']*'|[^\s]+/g) ?? []).map((t) => /^['"]/.test(t) ? t.slice(1, -1) : t);
54
+ }
55
+ async function packages(target) {
56
+ let patterns = [];
57
+ if (await present(join(target.repoRoot, 'pnpm-workspace.yaml'))) {
58
+ const config = YAML.parse(await readFile(join(target.repoRoot, 'pnpm-workspace.yaml'), 'utf8'));
59
+ patterns = config.packages?.filter((p) => !p.startsWith('!')) ?? [];
60
+ }
61
+ const match = createExclusionMatcher(patterns);
62
+ // A proper pattern prefix can still contain a member below it. A globstar
63
+ // prefix remains open at every depth. Reuse the membership matcher.
64
+ const descend = createExclusionMatcher(patterns.flatMap((pattern) => {
65
+ const parts = pattern.trim().replace(/\/+$/, '').split('/');
66
+ return parts.flatMap((part, index) => index < parts.length - 1 || part === '**'
67
+ ? [parts.slice(0, index + 1).join('/')]
68
+ : []);
69
+ }));
70
+ const excluded = createExclusionMatcher((await present(join(target.repoRoot, 'pnpm-workspace.yaml')))
71
+ ? (YAML.parse(await readFile(join(target.repoRoot, 'pnpm-workspace.yaml'), 'utf8')).packages ?? [])
72
+ .filter((p) => p.startsWith('!'))
73
+ .map((p) => p.slice(1))
74
+ : []);
75
+ const result = [];
76
+ const add = async (root) => {
77
+ if (!(await present(join(root, 'package.json'))))
78
+ return;
79
+ const p = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
80
+ result.push({ root, name: p.name, scripts: p.scripts });
81
+ };
82
+ await add(target.repoRoot);
83
+ if (target.appRoot !== target.repoRoot)
84
+ await add(target.appRoot);
85
+ if (patterns.length) {
86
+ const walk = async (root) => {
87
+ for (const e of await readdir(root, { withFileTypes: true })) {
88
+ if (!e.isDirectory() ||
89
+ e.name.startsWith('.') ||
90
+ e.name === 'node_modules')
91
+ continue;
92
+ const dir = join(root, e.name);
93
+ const rel = posix(relative(target.repoRoot, dir));
94
+ if (excluded.excludesDirectory(rel))
95
+ continue;
96
+ if (match.excludesDirectory(rel)) {
97
+ if (!result.some((p) => p.root === dir))
98
+ await add(dir);
99
+ }
100
+ // Package patterns, rather than arbitrary repo fixtures, define scope.
101
+ if (descend.excludesDirectory(rel))
102
+ await walk(dir);
103
+ }
104
+ };
105
+ await walk(target.repoRoot);
106
+ }
107
+ return result;
108
+ }
109
+ export async function checkCommands(target, pages, report) {
110
+ const docs = pages.filter((p) => /(?:^|\/)contributing(?:\.mdx?$|\/)/.test(p.path));
111
+ if (!docs.length)
112
+ return;
113
+ const manifests = await packages(target);
114
+ for (const p of docs)
115
+ for (const node of p.nodes) {
116
+ if (node.type !== 'code' && node.type !== 'inlineCode')
117
+ continue;
118
+ if (node.type === 'code' &&
119
+ node.lang &&
120
+ !['sh', 'shell', 'bash', 'zsh', 'console', 'powershell'].includes(node.lang))
121
+ continue;
122
+ let cwd = target.repoRoot;
123
+ const start = node.position?.start;
124
+ const openingLine = p.source.split(/\r?\n/).at((start?.line ?? 1) - 1) ?? '';
125
+ const fenced = node.type === 'code' &&
126
+ /^(?:`{3,}|~{3,})/.test(openingLine.slice((start?.column ?? 1) - 1));
127
+ for (const [lineIndex, original] of (node.value ?? '')
128
+ .split('\n')
129
+ .entries()) {
130
+ const line = (node.position?.start.line ?? 1) + lineIndex + (fenced ? 1 : 0);
131
+ // Split shell command boundaries while retaining quoted arguments. No
132
+ // subprocess is executed, and placeholders are never guessed.
133
+ const segments = original.match(/(?:"(?:\\.|[^"\\])*"|'[^']*'|[^;&\n])+/g) ?? [];
134
+ for (let segment of segments) {
135
+ segment = segment.replace(/^\s*\$\s+/, '').trim();
136
+ if (!segment || segment.startsWith('#'))
137
+ continue;
138
+ const words = tokens(segment);
139
+ if (words[0] === 'cd') {
140
+ cwd =
141
+ words[1] && !illustrative(words[1]) && cwd
142
+ ? resolve(cwd, words[1])
143
+ : undefined;
144
+ continue;
145
+ }
146
+ const manager = words.findIndex((w) => w === 'pnpm' || w === 'npm');
147
+ if (manager < 0)
148
+ continue;
149
+ const kind = words[manager];
150
+ let root = cwd;
151
+ let filter;
152
+ let index = manager + 1;
153
+ while (words[index]?.startsWith('-')) {
154
+ const option = words[index++];
155
+ if (['-w', '--workspace-root'].includes(option))
156
+ root = target.repoRoot;
157
+ else if (option === '--filter' || option === '-F')
158
+ filter = words[index++];
159
+ else if (option.startsWith('--filter='))
160
+ filter = option.slice(9);
161
+ else if (['--dir', '-C', '--prefix'].includes(option)) {
162
+ const dir = words[index++];
163
+ root =
164
+ root && dir && !illustrative(dir)
165
+ ? resolve(root, dir)
166
+ : undefined;
167
+ }
168
+ else if (['-r', '--recursive', '--workspace'].includes(option))
169
+ root = undefined;
170
+ }
171
+ let script = words[index];
172
+ if (!script)
173
+ continue;
174
+ if (kind === 'npm' &&
175
+ script !== 'run' &&
176
+ script !== 'run-script' &&
177
+ script !== 'test' &&
178
+ script !== 'start')
179
+ continue;
180
+ const explicitRun = script === 'run' || script === 'run-script';
181
+ if (explicitRun) {
182
+ index++;
183
+ while (words[index]?.startsWith('-'))
184
+ index++;
185
+ script = words[index];
186
+ }
187
+ else if (BUILTINS.has(script))
188
+ continue;
189
+ if (!script ||
190
+ illustrative(script) ||
191
+ (filter && illustrative(filter)))
192
+ continue;
193
+ let pkg;
194
+ if (filter) {
195
+ const selected = manifests.filter((item) => item.name === filter ||
196
+ (filter.startsWith('./') &&
197
+ posix(relative(target.repoRoot, item.root)) ===
198
+ filter.slice(2)));
199
+ if (selected.length === 1)
200
+ pkg = selected[0];
201
+ }
202
+ else if (root &&
203
+ inside(target.repoRoot, root) &&
204
+ (await present(root))) {
205
+ let current = root;
206
+ while (inside(target.repoRoot, current)) {
207
+ pkg = manifests.find((item) => item.root === current);
208
+ if (pkg || current === target.repoRoot)
209
+ break;
210
+ current = dirname(current);
211
+ }
212
+ }
213
+ if (!pkg)
214
+ report('commands', 'error', p.path, `Cannot resolve package context for ${kind} ${script}${filter ? ` (--filter ${filter})` : ''}; use literal cd, -w or --filter.`, line);
215
+ else if (!Object.hasOwn(pkg.scripts ?? {}, script)) {
216
+ // pnpm shorthand can invoke an installed local binary. Explicit run
217
+ // remains script-only; never execute a binary or search system PATH.
218
+ const installedBinary = kind === 'pnpm' &&
219
+ !explicitRun &&
220
+ !/[\\/]/.test(script) &&
221
+ script !== '.' &&
222
+ script !== '..' &&
223
+ (await Promise.all([pkg.root, target.repoRoot].map((binRoot) => present(join(binRoot, 'node_modules/.bin', script))))).some(Boolean);
224
+ if (!installedBinary)
225
+ report('commands', 'error', p.path, `Script "${script}" does not exist in ${posix(relative(target.repoRoot, join(pkg.root, 'package.json'))) || 'package.json'}.`, line);
226
+ }
227
+ else if (/^\s*echo(?:\s|$)/.test(pkg.scripts[script]))
228
+ report('commands', 'warn', p.path, `Script "${script}" is an echo stub in ${posix(relative(target.repoRoot, join(pkg.root, 'package.json')))}.`, line);
229
+ }
230
+ }
231
+ }
232
+ }
@@ -0,0 +1,5 @@
1
+ import type { Report } from './check.types.js';
2
+ import { type CheckTarget } from './files.js';
3
+ import { type ParsedPage } from './markdown.js';
4
+ export declare function checkDiagrams(target: CheckTarget, pages: ParsedPage[], report: Report): Promise<void>;
5
+ //# sourceMappingURL=diagrams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagrams.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/diagrams.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAsEpB,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA2Of"}
@@ -0,0 +1,197 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { basename, dirname, join, relative, resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { DOCS_CHECK_THRESHOLDS } from '../../../config/documentation-check.js';
6
+ import { containedFile, inside, posix, present, } from './files.js';
7
+ import { attributes, htmlNodes, parsePage, } from './markdown.js';
8
+ import { parseMermaid } from './mermaid-runtime.js';
9
+ const exec = promisify(execFile);
10
+ async function git(target, args) {
11
+ return (await exec('git', ['-C', target.repoRoot, ...args], {
12
+ maxBuffer: 16 * 1024 * 1024,
13
+ })).stdout;
14
+ }
15
+ function sourceDiagrams(page) {
16
+ return page.nodes.filter((n) => n.type === 'code' && n.lang?.toLowerCase() === 'mermaid');
17
+ }
18
+ function imageFile(target, page, href) {
19
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href))
20
+ return undefined;
21
+ let decoded;
22
+ try {
23
+ decoded = decodeURIComponent(href.split(/[?#]/)[0]);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ const root = decoded.startsWith('/')
29
+ ? join(target.appRoot, 'public')
30
+ : target.contentRoot;
31
+ const path = decoded.startsWith('/')
32
+ ? resolve(root, `.${decoded}`)
33
+ : resolve(target.contentRoot, dirname(page.path), decoded);
34
+ return inside(root, path) ? { root, path } : undefined;
35
+ }
36
+ function diagramSection(page, node, tabsOnly = true) {
37
+ const line = node.position?.start.line ?? 1;
38
+ const tabs = page.nodes
39
+ .filter((n) => n.name === 'Tabs' &&
40
+ (n.position?.start.line ?? Infinity) <= line &&
41
+ (n.position?.end.line ?? 0) >= line)
42
+ .at(-1);
43
+ if (tabsOnly && tabs?.position)
44
+ return { start: tabs.position.start.line, end: tabs.position.end.line + 1 };
45
+ // The containing authored section (and nested tab group) owns association;
46
+ // diagrams in another heading never satisfy this diagram's pair contract.
47
+ const headings = page.nodes.filter((n) => n.type === 'heading' && (n.depth ?? 7) <= 3);
48
+ const before = headings
49
+ .filter((n) => (n.position?.start.line ?? 1) < line)
50
+ .at(-1);
51
+ const after = headings.find((n) => (n.position?.start.line ?? 1) > line &&
52
+ (n.depth ?? 7) <= (before?.depth ?? 3));
53
+ return {
54
+ start: before?.position?.start.line ?? 1,
55
+ end: after?.position?.start.line ?? Infinity,
56
+ };
57
+ }
58
+ export async function checkDiagrams(target, pages, report) {
59
+ const altMax = target.documentation.check?.thresholds?.altMax ??
60
+ DOCS_CHECK_THRESHOLDS.altMax;
61
+ const assets = new Map();
62
+ const associations = [];
63
+ for (const page of pages) {
64
+ for (const image of page.links.filter((l) => l.image)) {
65
+ const alt = image.alt?.trim() ?? '';
66
+ const slug = basename(image.href)
67
+ .replace(/\.[^.]+$/, '')
68
+ .replace(/[-_]/g, ' ')
69
+ .toLowerCase();
70
+ if (!alt ||
71
+ alt.length > altMax ||
72
+ !/\s/.test(alt) ||
73
+ alt.toLowerCase().replace(/[-_]/g, ' ') === slug)
74
+ report('image-alt', 'warn', page.path, `Image alt text is empty, filename/slug-like or longer than ${altMax} characters.`, image.line);
75
+ const asset = imageFile(target, page, image.href);
76
+ if (!asset || !/\.svg$/i.test(asset.path))
77
+ continue;
78
+ if (/(?:^|[\\/])diagrams[\\/]/.test(asset.path) ||
79
+ /-(light|dark)\.svg$/.test(asset.path))
80
+ assets.set(asset.path, asset);
81
+ if (/-(light|dark)\.svg$/.test(asset.path)) {
82
+ const other = asset.path.replace(/-(light|dark)\.svg$/, (_, theme) => `-${theme === 'light' ? 'dark' : 'light'}.svg`);
83
+ const referenced = page.links.some((l) => l.image && imageFile(target, page, l.href)?.path === other);
84
+ if (!referenced)
85
+ report('diagram-pairs', 'error', page.path, `Diagram must reference both light and dark SVGs: ${basename(asset.path)}.`, image.line);
86
+ }
87
+ const section = diagramSection(page, {
88
+ type: 'image',
89
+ position: {
90
+ start: { line: image.line, column: 1 },
91
+ end: { line: image.line, column: 1 },
92
+ },
93
+ }, false);
94
+ const description = page.nodes.some((n) => n.type === 'paragraph' &&
95
+ (n.position?.start.line ?? 0) >= section.start &&
96
+ (n.position?.end.line ?? Infinity) < section.end &&
97
+ n.children?.some((c) => c.type === 'text' && c.value?.trim()));
98
+ const associated = image.describedBy
99
+ ?.split(/\s+/)
100
+ .some((id) => page.anchors.has(id));
101
+ if (assets.has(asset.path) && !description && !associated)
102
+ report('diagram-accessibility', 'warn', page.path, 'Rendered diagram needs page-visible explanatory text or an associated HTML long description; SVG desc alone is insufficient.', image.line);
103
+ }
104
+ for (const node of sourceDiagrams(page)) {
105
+ const line = node.position?.start.line ?? 1;
106
+ try {
107
+ await parseMermaid(node.value ?? '');
108
+ }
109
+ catch (error) {
110
+ report('mermaid', 'error', page.path, `Invalid Mermaid: ${error.message}`, line);
111
+ }
112
+ if (!/^\s*accTitle\s*:/m.test(node.value ?? '') ||
113
+ !/^\s*accDescr\s*(?::|\{)/m.test(node.value ?? ''))
114
+ report('diagram-accessibility', 'warn', page.path, 'Mermaid needs accTitle and accDescr for accessibility.', line);
115
+ const section = diagramSection(page, node);
116
+ const pairs = page.links
117
+ .filter((l) => l.image && l.line >= section.start && l.line < section.end)
118
+ .map((l) => imageFile(target, page, l.href)?.path)
119
+ .filter((p) => !!p && /-(light|dark)\.svg$/.test(p));
120
+ if (pairs.length)
121
+ associations.push({ page, node, pairs });
122
+ }
123
+ }
124
+ // Inspect only conventional diagram directories or explicit paired refs;
125
+ // ordinary SVG logos remain ordinary assets.
126
+ for (const root of [join(target.appRoot, 'public'), target.contentRoot]) {
127
+ const diagrams = join(root, 'diagrams');
128
+ if (!(await present(diagrams)))
129
+ continue;
130
+ const walk = async (dir) => {
131
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
132
+ const path = join(dir, entry.name);
133
+ if (entry.isDirectory())
134
+ await walk(path);
135
+ else if (entry.isFile() && path.endsWith('.svg'))
136
+ assets.set(path, { root, path });
137
+ }
138
+ };
139
+ await walk(diagrams);
140
+ }
141
+ for (const { root, path } of assets.values()) {
142
+ const label = posix(relative(target.contentRoot, path));
143
+ if (!(await containedFile(root, path))) {
144
+ report('diagram-pairs', 'error', label, 'Referenced diagram SVG does not exist inside its asset root.');
145
+ continue;
146
+ }
147
+ if (/-(light|dark)\.svg$/.test(path)) {
148
+ const sibling = path.replace(/-(light|dark)\.svg$/, (_, theme) => `-${theme === 'light' ? 'dark' : 'light'}.svg`);
149
+ if (!(await containedFile(root, sibling)))
150
+ report('diagram-pairs', 'error', label, 'Diagram SVG is missing its light/dark sibling.');
151
+ }
152
+ const nodes = htmlNodes(await readFile(path, 'utf8'));
153
+ const svg = nodes.find((n) => n.nodeName === 'svg');
154
+ const title = nodes.find((n) => n.nodeName === 'title' && n.childNodes?.some((c) => c.value?.trim()));
155
+ const desc = nodes.find((n) => n.nodeName === 'desc' && n.childNodes?.some((c) => c.value?.trim()));
156
+ if (!svg || attributes(svg).role !== 'img' || !title || !desc)
157
+ report('diagram-accessibility', 'error', label, 'Diagram SVG requires role="img", a nonempty title and desc.');
158
+ }
159
+ if (!associations.length)
160
+ return;
161
+ const base = target.documentation.check?.diagramBaseRef ?? 'origin/main';
162
+ try {
163
+ const mergeBase = (await git(target, ['merge-base', 'HEAD', base])).trim();
164
+ const committed = (await git(target, ['diff', '--name-only', '-z', mergeBase, 'HEAD', '--'])).split('\0');
165
+ const working = (await git(target, ['diff', '--name-only', '-z', 'HEAD', '--'])).split('\0');
166
+ const untracked = (await git(target, [
167
+ 'ls-files',
168
+ '--others',
169
+ '--exclude-standard',
170
+ '-z',
171
+ '--',
172
+ ])).split('\0');
173
+ const changed = new Set([...committed, ...working, ...untracked]);
174
+ for (const { page, node, pairs } of associations) {
175
+ const sourcePath = posix(relative(target.repoRoot, join(target.contentRoot, page.path)));
176
+ if (!changed.has(sourcePath))
177
+ continue;
178
+ let old;
179
+ try {
180
+ old = parsePage(page.path, await git(target, ['show', `${mergeBase}:${sourcePath}`]));
181
+ }
182
+ catch {
183
+ /* new page */
184
+ }
185
+ const current = sourceDiagrams(page).map((n) => n.value);
186
+ const previous = old ? sourceDiagrams(old).map((n) => n.value) : [];
187
+ const index = sourceDiagrams(page).indexOf(node);
188
+ if (current[index] === previous[index] || previous.includes(node.value))
189
+ continue;
190
+ if (pairs.some((path) => !changed.has(posix(relative(target.repoRoot, path)))))
191
+ report('diagram-freshness', 'warn', page.path, 'Mermaid source changed without both associated SVGs in the same committed/working-tree change.', node.position?.start.line);
192
+ }
193
+ }
194
+ catch {
195
+ report('diagram-freshness', 'warn', 'index.md', `Diagram diff check unverified: no usable Git merge base for "${base}"; static checks still ran.`);
196
+ }
197
+ }
@@ -0,0 +1,21 @@
1
+ import { type ExclusionMatcher } from '../../docs/shared/exclusions.js';
2
+ import { type OatDocumentationConfig } from '../../../config/oat-config.js';
3
+ import { type ParsedPage } from './markdown.js';
4
+ export declare function posix(path: string): string;
5
+ export declare function inside(root: string, path: string): boolean;
6
+ export declare function present(path: string): Promise<boolean>;
7
+ export interface CheckTarget {
8
+ repoRoot: string;
9
+ appRoot: string;
10
+ contentRoot: string;
11
+ framework: 'markdown' | 'fumadocs' | 'mkdocs';
12
+ documentation: OatDocumentationConfig;
13
+ matcher: ExclusionMatcher;
14
+ }
15
+ export declare function resolveCheckTarget(cwd: string, targetDir?: string): Promise<CheckTarget>;
16
+ export declare function readerFiles(target: CheckTarget): Promise<string[]>;
17
+ export declare function readPage(target: CheckTarget, path: string): Promise<ParsedPage>;
18
+ /** Never follow a link outside its allowed root, including symlink escapes. */
19
+ export declare function containedFile(root: string, path: string): Promise<boolean>;
20
+ export declare function readerDirectories(paths: string[]): string[];
21
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +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;CAC3B;AACD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAiEtB;AACD,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsBxE;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"}