@ankhorage/paradox 0.1.27 → 0.2.0

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 (58) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +53 -61
  3. package/dist/analyze/analyze.d.ts +2 -2
  4. package/dist/analyze/analyze.js +51 -35
  5. package/dist/analyze/badges.d.ts +2 -1
  6. package/dist/analyze/badges.js +14 -4
  7. package/dist/analyze/components.js +12 -11
  8. package/dist/analyze/documentation/collectDocumentationCommentsAsync.d.ts +11 -0
  9. package/dist/analyze/documentation/collectDocumentationCommentsAsync.js +72 -0
  10. package/dist/analyze/documentation/findings.d.ts +5 -0
  11. package/dist/analyze/documentation/findings.js +17 -0
  12. package/dist/analyze/documentation/validateDocumentationPolicyAsync.d.ts +13 -0
  13. package/dist/analyze/documentation/validateDocumentationPolicyAsync.js +150 -0
  14. package/dist/analyze/documentation/validateReferencesAsync.d.ts +11 -0
  15. package/dist/analyze/documentation/validateReferencesAsync.js +148 -0
  16. package/dist/analyze/exports.d.ts +4 -0
  17. package/dist/analyze/exports.js +14 -7
  18. package/dist/analyze/readmeConfig.d.ts +1 -3
  19. package/dist/analyze/readmeConfig.js +8 -19
  20. package/dist/analyze/readmeUsage.d.ts +7 -10
  21. package/dist/analyze/readmeUsage.js +124 -48
  22. package/dist/analyze/semantic/docBlocks.js +18 -48
  23. package/dist/analyze/semantic/exports.js +1 -3
  24. package/dist/analyze/semantic/model.d.ts +0 -2
  25. package/dist/analyze/semantic/paradoxComment.d.ts +1 -11
  26. package/dist/analyze/semantic/paradoxComment.js +1 -43
  27. package/dist/analyze/semantic/tagRegistry.js +2 -1
  28. package/dist/analyze/sourceFunctions.js +4 -4
  29. package/dist/analyze/types.d.ts +31 -24
  30. package/dist/analyze/usage.d.ts +2 -2
  31. package/dist/analyze/usage.js +3 -33
  32. package/dist/analyze/utils/getExportMetadata.js +11 -40
  33. package/dist/analyze/utils/parseParadoxComment.d.ts +12 -9
  34. package/dist/analyze/utils/parseParadoxComment.js +66 -78
  35. package/dist/cli/index.d.ts +3 -2
  36. package/dist/cli/index.js +3 -2
  37. package/dist/cli/standalone.js +11 -0
  38. package/dist/config/defineParadoxConfig.d.ts +1 -1
  39. package/dist/doc-tags/registry.d.ts +28 -32
  40. package/dist/doc-tags/registry.js +35 -39
  41. package/dist/index.d.ts +1 -1
  42. package/dist/model/buildModel.d.ts +26 -19
  43. package/dist/model/buildModel.js +33 -99
  44. package/dist/model/types.d.ts +27 -20
  45. package/dist/paths/policy.d.ts +1 -1
  46. package/dist/render/renderers/diagrams.js +1 -7
  47. package/dist/render/renderers/html.js +89 -58
  48. package/dist/render/renderers/markdown.js +139 -86
  49. package/dist/render/toFileStem.d.ts +2 -0
  50. package/dist/render/toFileStem.js +8 -0
  51. package/dist/{config/types.d.ts → types/config.d.ts} +3 -5
  52. package/dist/write/write.d.ts +1 -1
  53. package/package.json +2 -1
  54. package/dist/analyze/readmeCli.d.ts +0 -9
  55. package/dist/analyze/readmeCli.js +0 -33
  56. package/dist/analyze/utils/getLeadingParadoxComment.d.ts +0 -10
  57. package/dist/analyze/utils/getLeadingParadoxComment.js +0 -16
  58. /package/dist/{config/types.js → types/config.js} +0 -0
@@ -4,7 +4,6 @@ import { slugifyAscii } from '@ankhorage/utility/string';
4
4
  */
5
5
  export function renderHtml({ diagrams, model, }) {
6
6
  const sourceAreas = getSourceAreas(model);
7
- const cliScenarios = getReadmeCliScenarios(model);
8
7
  const exportsByModule = groupBy(model.exports, (item) => item.modulePath);
9
8
  return {
10
9
  indexHtml: `<!doctype html>
@@ -141,7 +140,7 @@ export function renderHtml({ diagrams, model, }) {
141
140
  </aside>
142
141
  <main class="content">
143
142
  <section id="view-home" class="view" data-view="home">
144
- ${renderHomeView(model, diagrams, cliScenarios, exportsByModule)}
143
+ ${renderHomeView(model, diagrams, exportsByModule)}
145
144
  </section>
146
145
  ${sourceAreas.map(renderSourceAreaView).join('')}
147
146
  </main>
@@ -197,7 +196,7 @@ export function renderHtml({ diagrams, model, }) {
197
196
  /***
198
197
  * Renders the Home view that keeps public API and package-level information together.
199
198
  */
200
- function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
199
+ function renderHomeView(model, diagrams, exportsByModule) {
201
200
  return `
202
201
  <section class="panel">
203
202
  <h2>Package overview</h2>
@@ -212,7 +211,8 @@ function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
212
211
  ${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
213
212
  </ul>
214
213
  </section>
215
- ${model.readmeCli !== null ? renderCliPanel(model, diagrams, cliScenarios) : ''}
214
+ ${renderUsagePanel(model)}
215
+ ${renderFindingsPanel(model)}
216
216
  <section class="panel">
217
217
  <h2>Modules</h2>
218
218
  ${model.modules.map(renderModuleCard).join('')}
@@ -237,36 +237,62 @@ function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
237
237
  </section>`;
238
238
  }
239
239
  /***
240
- * Renders the Home CLI chapter for detected bin scenarios.
240
+ * Renders complete CLI and programmatic usage documentation.
241
241
  */
242
- function renderCliPanel(model, diagrams, scenarios) {
243
- const commands = model.usage?.commands ?? [];
244
- const searchText = [
245
- 'cli',
246
- model.readmeCli?.description ?? '',
247
- ...commands.map((command) => command.command),
248
- ...scenarios.map((scenario) => scenario.name),
249
- ].join(' ');
250
- return `<section class="panel" data-search="${escapeAttribute(searchText)}">
251
- <h2>CLI</h2>
252
- ${model.readmeCli?.description === null || model.readmeCli?.description === undefined ? '' : `<p>${escapeHtml(model.readmeCli.description)}</p>`}
253
- ${commands.length === 0 ? '' : `<pre>${escapeHtml(commands.map((command) => command.command).join('\n'))}</pre>`}
254
- ${scenarios
255
- .map((scenario) => {
256
- const diagram = findScenarioDiagram(diagrams, scenario);
257
- if (scenario.description === null && diagram === undefined)
258
- return '';
259
- return `<article class="item" data-search="${escapeAttribute([scenario.name, scenario.description ?? ''].join(' '))}">
260
- <h3>${escapeHtml(scenario.name)}</h3>
261
- ${scenario.description === null ? '' : `<p>${escapeHtml(scenario.description)}</p>`}
262
- ${diagram === undefined ? '' : renderDiagramCard(diagram)}
263
- </article>`;
264
- })
265
- .join('')}
242
+ function renderUsagePanel(model) {
243
+ return `<section class="panel" data-search="${escapeAttribute([
244
+ 'usage',
245
+ model.usage.command,
246
+ ...model.usageEntries.flatMap((entry) => [
247
+ entry.title ?? '',
248
+ entry.description ?? '',
249
+ entry.sourcePath,
250
+ ]),
251
+ ].join(' '))}">
252
+ <h2>Usage</h2>
253
+ <article class="item">
254
+ <h3>CLI</h3>
255
+ <pre>${escapeHtml(model.usage.command)}</pre>
256
+ </article>
257
+ ${model.usageEntries.map(renderUsageEntry).join('')}
266
258
  </section>`;
267
259
  }
268
260
  /***
269
- * Renders one source file entry in the left navigation.
261
+ * Renders one source-backed usage entry in the complete documentation app.
262
+ */
263
+ function renderUsageEntry(entry) {
264
+ return `<article class="item" data-search="${escapeAttribute([
265
+ entry.title ?? '',
266
+ entry.description ?? '',
267
+ entry.sourcePath,
268
+ ...entry.see,
269
+ ...entry.security,
270
+ ].join(' '))}">
271
+ <h3>${escapeHtml(entry.title ?? 'Usage')}</h3>
272
+ <p class="muted"><code>${escapeHtml(entry.sourcePath)}</code></p>
273
+ ${entry.description === null ? '' : `<p>${escapeHtml(entry.description)}</p>`}
274
+ ${renderReferenceMetadata(entry)}
275
+ <pre>${escapeHtml(entry.code)}</pre>
276
+ </article>`;
277
+ }
278
+ /***
279
+ * Renders policy findings so complete docs expose the same evidence consumed by Doctor.
280
+ */
281
+ function renderFindingsPanel(model) {
282
+ if (model.findings.length === 0) {
283
+ return '<section class="panel"><h2>Policy</h2><p>No documentation findings.</p></section>';
284
+ }
285
+ return `<section class="panel">
286
+ <h2>Policy findings</h2>
287
+ ${model.findings
288
+ .map((finding) => `<article class="item" data-search="${escapeAttribute([finding.ruleId, finding.severity, finding.message, finding.sourcePath ?? ''].join(' '))}">
289
+ <h3>${escapeHtml(finding.ruleId)}</h3>
290
+ <p><strong>${escapeHtml(finding.severity)}</strong> — ${escapeHtml(finding.message)}</p>
291
+ ${finding.sourcePath === null ? '' : `<p class="muted"><code>${escapeHtml(finding.sourcePath)}${finding.line === null ? '' : `:${finding.line}`}</code></p>`}
292
+ </article>`)
293
+ .join('')}
294
+ </section>`;
295
+ }
270
296
  /***
271
297
  * Renders one source file entry in the left navigation.
272
298
  */
@@ -293,10 +319,17 @@ function renderSourceAreaView(area) {
293
319
  * Renders one source function card in a source-area view.
294
320
  */
295
321
  function renderSourceFunctionCard(item) {
296
- return `<article class="item" data-search="${escapeAttribute([item.name, item.sourceLocation.filePath, item.description ?? ''].join(' '))}">
322
+ return `<article class="item" data-search="${escapeAttribute([
323
+ item.name,
324
+ item.sourceLocation.filePath,
325
+ item.description ?? '',
326
+ ...item.see,
327
+ ...item.security,
328
+ ].join(' '))}">
297
329
  <h3>${escapeHtml(item.name)}</h3>
298
330
  <p class="muted"><code>${escapeHtml(item.sourceLocation.filePath)}:${item.sourceLocation.line}:${item.sourceLocation.column}</code></p>
299
331
  ${item.description === null ? '<p class="empty">No description available.</p>' : `<p>${escapeHtml(item.description)}</p>`}
332
+ ${renderReferenceMetadata(item)}
300
333
  </article>`;
301
334
  }
302
335
  /***
@@ -307,22 +340,6 @@ function getSourceAreas(model) {
307
340
  .map(([path, functions]) => ({ path, functions }))
308
341
  .sort((left, right) => left.path.localeCompare(right.path));
309
342
  }
310
- /***
311
- * Selects bin scenarios that should be shown on the Home page.
312
- */
313
- function getReadmeCliScenarios(model) {
314
- if (model.readmeCli === null)
315
- return [];
316
- return model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin');
317
- }
318
- /***
319
- * Finds the generated Mermaid artifact for a sequence scenario.
320
- /***
321
- * Finds the generated Mermaid artifact for a sequence scenario.
322
- */
323
- function findScenarioDiagram(diagrams, scenario) {
324
- return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
325
- }
326
343
  /***
327
344
  * Renders module metadata on the Home page.
328
345
  */
@@ -340,15 +357,20 @@ function renderModuleCard(module) {
340
357
  function renderExportCard(item) {
341
358
  return `<article class="item" id="symbol-${slugifyAscii(item.name)}" data-search="${escapeAttribute([
342
359
  item.name,
360
+ item.title ?? '',
343
361
  item.kind,
344
362
  item.modulePath,
345
363
  item.description ?? '',
364
+ ...item.see,
365
+ ...item.security,
346
366
  ...item.relatedSymbols,
347
367
  ...item.signatures.map((signature) => signature.label),
348
368
  ].join(' '))}">
349
- <h4>${escapeHtml(item.name)}</h4>
369
+ <h4>${escapeHtml(item.title ?? item.name)}</h4>
370
+ ${item.title === null || item.title === item.name ? '' : `<p class="muted">Symbol: <code>${escapeHtml(item.name)}</code></p>`}
350
371
  <p class="muted">${escapeHtml(item.kind)} • <code>${escapeHtml(item.sourceLocation.filePath)}:${item.sourceLocation.line}:${item.sourceLocation.column}</code></p>
351
372
  ${item.description ? `<p>${escapeHtml(item.description)}</p>` : ''}
373
+ ${renderReferenceMetadata(item)}
352
374
  <p><strong>Export paths:</strong> ${renderInlineCodeList(item.exportPaths)}</p>
353
375
  <div><strong>Related symbols:</strong> ${item.relatedSymbols.length > 0 ? renderChipList(item.relatedSymbols) : '<span class="empty">None</span>'}</div>
354
376
  ${item.signatures.length > 0 ? renderSignatureBlock(item) : ''}
@@ -409,11 +431,14 @@ function renderComponentCard(component) {
409
431
  component.name,
410
432
  component.modulePath,
411
433
  component.description ?? '',
434
+ ...component.see,
435
+ ...component.security,
412
436
  ...component.props.map((prop) => `${prop.name} ${prop.type}`),
413
437
  ].join(' '))}">
414
438
  <h3>${escapeHtml(component.name)}</h3>
415
439
  <p class="muted"><code>${escapeHtml(component.sourceLocation.filePath)}:${component.sourceLocation.line}:${component.sourceLocation.column}</code></p>
416
440
  ${component.description ? `<p>${escapeHtml(component.description)}</p>` : ''}
441
+ ${renderReferenceMetadata(component)}
417
442
  <p><strong>Export paths:</strong> ${renderInlineCodeList(component.exportPaths)}</p>
418
443
  <table>
419
444
  <thead><tr><th>Prop</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
@@ -444,6 +469,22 @@ function renderDiagramCard(diagram) {
444
469
  </details>
445
470
  </article>`;
446
471
  }
472
+ /***
473
+ * Renders validated external references and security-test evidence.
474
+ */
475
+ function renderReferenceMetadata(metadata) {
476
+ const see = metadata.see.length === 0
477
+ ? ''
478
+ : `<p><strong>See also:</strong> ${metadata.see
479
+ .map((url) => `<a href="${escapeAttribute(url)}" rel="noreferrer">${escapeHtml(url)}</a>`)
480
+ .join(', ')}</p>`;
481
+ const security = metadata.security.length === 0
482
+ ? ''
483
+ : `<p><strong>Security tests:</strong> ${metadata.security
484
+ .map((reference) => `<code>${escapeHtml(reference)}</code>`)
485
+ .join(', ')}</p>`;
486
+ return `${see}${security}`;
487
+ }
447
488
  /***
448
489
  * Renders an inline comma-separated list of code values.
449
490
  */
@@ -478,16 +519,6 @@ function groupBy(items, key) {
478
519
  }
479
520
  return new Map([...groups.entries()].sort(([left], [right]) => left.localeCompare(right)));
480
521
  }
481
- /***
482
- * Converts a scenario name to the generated Mermaid file stem.
483
- */
484
- function toFileStem(value) {
485
- return value
486
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
487
- .replace(/[^A-Za-z0-9]+/g, '-')
488
- .replace(/^-+|-+$/g, '')
489
- .toLowerCase();
490
- }
491
522
  /***
492
523
  * Escapes user-controlled text for safe HTML rendering.
493
524
  */
@@ -8,6 +8,9 @@ export function renderMarkdown({ badges, diagrams, model, outputDir, }) {
8
8
  components: renderComponents(model),
9
9
  };
10
10
  }
11
+ /***
12
+ * Renders the generated package README.
13
+ */
11
14
  function renderReadme(model, outputDir, badges, diagrams) {
12
15
  const lines = [
13
16
  '<!-- markdownlint-disable MD013 MD033 -->',
@@ -23,77 +26,67 @@ function renderReadme(model, outputDir, badges, diagrams) {
23
26
  }
24
27
  if (model.description)
25
28
  lines.push(model.description, '');
26
- renderReadmeUsage(lines, model.readmeUsageDescription, model.readmeUsage);
27
- renderReadmeCli(lines, model, outputDir, diagrams);
29
+ renderUsage(lines, model);
28
30
  renderConfiguration(lines, model);
29
31
  renderGeneratedDocumentation(lines, outputDir, diagrams);
30
32
  renderReadmeApi(lines, model);
31
33
  return `${lines.join('\n').trimEnd()}\n`;
32
34
  }
33
- function renderReadmeUsage(lines, description, entries) {
34
- if (description === null && entries.length === 0)
35
- return;
35
+ /***
36
+ * Renders the canonical CLI-first Usage chapter.
37
+ */
38
+ function renderUsage(lines, model) {
39
+ const readmeExample = model.usageEntries.find((entry) => entry.area === 'examples' && entry.isReadme);
36
40
  lines.push('## Usage', '');
37
- if (description !== null)
38
- lines.push(description, '');
39
- for (const entry of entries) {
40
- if (entry.title !== null)
41
- lines.push(`### ${entry.title}`, '');
42
- if (entry.description !== null) {
43
- const [, ...rest] = entry.description.split('\n');
44
- const entryDescription = rest.join('\n').trim();
45
- if (entryDescription.length > 0)
46
- lines.push(entryDescription, '');
47
- }
48
- lines.push(`Source: \`${entry.sourcePath}\``, '');
49
- lines.push(`\`\`\`${entry.language}`);
50
- lines.push(entry.code);
51
- lines.push('```', '');
52
- }
53
- }
54
- function renderReadmeCli(lines, model, outputDir, diagrams) {
55
- if (model.readmeCli === null)
41
+ lines.push('### CLI', '');
42
+ lines.push('Ankhorage packages expose their command-line interface through `ankh`. Use `ankh --help` to discover available package commands, or run a package command with `--help` for package-specific usage.', '');
43
+ lines.push('```zsh');
44
+ lines.push('# Install the Ankhorage CLI');
45
+ lines.push('bun add --global @ankhorage/ankh', '');
46
+ lines.push(`# Show usage information for ${getPackageDisplayName(model.packageId)}`);
47
+ lines.push(model.usage.command);
48
+ lines.push('```', '');
49
+ if (readmeExample === undefined)
56
50
  return;
57
- lines.push('## CLI', '');
58
- if (model.readmeCli.description !== null)
59
- lines.push(model.readmeCli.description, '');
60
- if (model.usage !== null && model.usage.commands.length > 0) {
61
- lines.push('```bash');
62
- for (const command of model.usage.commands)
63
- lines.push(command.command);
64
- lines.push('```', '');
65
- }
66
- const scenarios = model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin');
67
- for (const scenario of scenarios) {
68
- const diagram = findScenarioDiagram(diagrams, scenario);
69
- if (scenario.description === null && diagram === undefined)
70
- continue;
71
- lines.push('<details>');
72
- lines.push(`<summary>${scenario.name}</summary>`, '');
73
- if (scenario.description !== null)
74
- lines.push(scenario.description, '');
75
- if (diagram !== undefined) {
76
- lines.push(`Diagram: [${diagram.title}](./${outputDir}/${diagram.path})`, '');
77
- lines.push('```mermaid');
78
- lines.push(diagram.content.trimEnd());
79
- lines.push('```', '');
80
- }
81
- lines.push('</details>', '');
51
+ lines.push(`### ${readmeExample.title ?? 'Programmatic Usage'}`, '');
52
+ if (readmeExample.description !== null)
53
+ lines.push(readmeExample.description, '');
54
+ renderReferences(lines, readmeExample);
55
+ lines.push('```' + readmeExample.language);
56
+ lines.push(readmeExample.code);
57
+ lines.push('```', '');
58
+ if (model.exampleCount > 1) {
59
+ const additionalExamples = model.exampleCount - 1;
60
+ const label = additionalExamples === 1 ? 'example' : 'examples';
61
+ lines.push(`This package contains ${additionalExamples} additional ${label}. See the generated documentation for the complete set.`, '');
82
62
  }
83
63
  }
84
- function findScenarioDiagram(diagrams, scenario) {
85
- return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
64
+ /***
65
+ * Returns a human-readable package command name.
66
+ */
67
+ function getPackageDisplayName(packageId) {
68
+ return packageId.split('/').pop() ?? packageId;
86
69
  }
70
+ /***
71
+ * Renders the canonical Configuration chapter from the tagged schema plus concrete config instance.
72
+ */
87
73
  function renderConfiguration(lines, model) {
88
74
  const config = model.config?.isReadme ? model.config : null;
89
75
  const example = model.readmeConfig;
90
76
  if (config === null && example === null)
91
77
  return;
92
78
  lines.push('## Configuration', '');
79
+ if (config !== null) {
80
+ if (config.title !== null && config.title !== 'Configuration') {
81
+ lines.push(`### ${config.title}`, '');
82
+ }
83
+ if (config.description !== null)
84
+ lines.push(config.description, '');
85
+ renderReferences(lines, config);
86
+ }
93
87
  if (example !== null) {
94
- if (example.description !== null)
95
- lines.push(example.description, '');
96
- lines.push(`\`\`\`${example.language}`);
88
+ lines.push('### Example', '');
89
+ lines.push('```' + example.language);
97
90
  lines.push(example.code);
98
91
  lines.push('```', '');
99
92
  }
@@ -108,15 +101,33 @@ function renderConfiguration(lines, model) {
108
101
  }
109
102
  lines.push('', '</details>', '');
110
103
  }
104
+ /***
105
+ * Renders links and security evidence owned by one documented item.
106
+ */
107
+ function renderReferences(lines, metadata) {
108
+ if (metadata.see.length > 0) {
109
+ lines.push(`See also: ${metadata.see.map((url) => `[${url}](${url})`).join(', ')}`, '');
110
+ }
111
+ if (metadata.security.length > 0) {
112
+ lines.push(`Security tests: ${metadata.security.map((reference) => `\`${reference}\``).join(', ')}`, '');
113
+ }
114
+ }
115
+ /***
116
+ * Renders links to generated documentation artifacts.
117
+ */
111
118
  function renderGeneratedDocumentation(lines, outputDir, diagrams) {
112
119
  lines.push('## Generated documentation', '');
113
120
  lines.push(`- [Interactive documentation app](./${outputDir}/index.html)`);
114
121
  lines.push(`- [Public API reference](./${outputDir}/exports.md)`);
115
122
  lines.push(`- [Component registry](./${outputDir}/components.md)`);
116
- for (const diagram of diagrams)
123
+ for (const diagram of diagrams) {
117
124
  lines.push(`- [${diagram.title}](./${outputDir}/${diagram.path})`);
125
+ }
118
126
  lines.push('');
119
127
  }
128
+ /***
129
+ * Renders README-promoted public API entries.
130
+ */
120
131
  function renderReadmeApi(lines, model) {
121
132
  const groups = getReadmeGroups(model);
122
133
  if (groups.length === 0)
@@ -125,20 +136,25 @@ function renderReadmeApi(lines, model) {
125
136
  for (const group of groups) {
126
137
  lines.push(`### ${group.title}`, '');
127
138
  for (const item of group.items) {
128
- if (item.kind === 'component')
139
+ if (item.kind === 'component') {
129
140
  renderComponentAccordion(lines, item.component, item.exportEntry);
130
- else
141
+ }
142
+ else {
131
143
  renderExportAccordion(lines, item.exportEntry);
144
+ }
132
145
  }
133
146
  }
134
147
  }
148
+ /***
149
+ * Renders one README-promoted component.
150
+ */
135
151
  function renderComponentAccordion(lines, component, exportEntry) {
136
152
  lines.push('<details>');
137
- lines.push(`<summary>${component.name}</summary>`, '');
153
+ lines.push(`<summary>${exportEntry?.title ?? component.name}</summary>`, '');
138
154
  renderSignature(lines, exportEntry);
139
155
  if (component.description)
140
156
  lines.push(component.description, '');
141
- renderExamples(lines, component.examples);
157
+ renderReferences(lines, component);
142
158
  if (exportEntry && exportEntry.relatedSymbols.length > 0) {
143
159
  lines.push(`Related types: ${exportEntry.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`, '');
144
160
  }
@@ -154,13 +170,16 @@ function renderComponentAccordion(lines, component, exportEntry) {
154
170
  }
155
171
  lines.push('</details>', '');
156
172
  }
173
+ /***
174
+ * Renders one README-promoted non-component export.
175
+ */
157
176
  function renderExportAccordion(lines, item) {
158
177
  lines.push('<details>');
159
- lines.push(`<summary>${item.name}</summary>`, '');
178
+ lines.push(`<summary>${item.title ?? item.name}</summary>`, '');
160
179
  renderSignature(lines, item);
161
180
  lines.push(item.description ?? `\`${item.kind}\` export.`, '');
181
+ renderReferences(lines, item);
162
182
  renderStructuredRows(lines, item);
163
- renderExamples(lines, item.examples);
164
183
  lines.push(`Module: \`${item.modulePath}\``);
165
184
  lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``);
166
185
  if (item.relatedSymbols.length > 0) {
@@ -168,6 +187,9 @@ function renderExportAccordion(lines, item) {
168
187
  }
169
188
  lines.push('', '</details>', '');
170
189
  }
190
+ /***
191
+ * Renders the primary signature for a README public API entry.
192
+ */
171
193
  function renderSignature(lines, item) {
172
194
  const signature = item?.signatures[0]?.label;
173
195
  if (!signature)
@@ -176,15 +198,9 @@ function renderSignature(lines, item) {
176
198
  lines.push(`${item.name}${signature}`);
177
199
  lines.push('```', '');
178
200
  }
179
- function renderExamples(lines, examples) {
180
- for (const example of examples) {
181
- if (example.title)
182
- lines.push(`#### ${example.title}`, '');
183
- lines.push(`\`\`\`${example.language ?? ''}`);
184
- lines.push(example.code);
185
- lines.push('```', '');
186
- }
187
- }
201
+ /***
202
+ * Renders structured const-array rows as a Markdown table.
203
+ */
188
204
  function renderStructuredRows(lines, item) {
189
205
  if (item.structuredRows.length === 0)
190
206
  return;
@@ -202,6 +218,9 @@ function renderStructuredRows(lines, item) {
202
218
  }
203
219
  lines.push('');
204
220
  }
221
+ /***
222
+ * Returns stable structured-row columns.
223
+ */
205
224
  function getStructuredColumns(item) {
206
225
  const columns = new Set();
207
226
  for (const row of item.structuredRows) {
@@ -210,9 +229,15 @@ function getStructuredColumns(item) {
210
229
  }
211
230
  return [...columns];
212
231
  }
232
+ /***
233
+ * Formats one structured-row column header.
234
+ */
213
235
  function formatStructuredColumnHeader(column) {
214
236
  return escapeTableCell(column.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase());
215
237
  }
238
+ /***
239
+ * Formats one structured-row table cell.
240
+ */
216
241
  function formatStructuredCell(column, value) {
217
242
  const escaped = escapeTableCell(value);
218
243
  if (column === 'syntax' || column === 'name' || column === 'handler')
@@ -223,6 +248,9 @@ function formatStructuredCell(column, value) {
223
248
  return 'no';
224
249
  return escaped;
225
250
  }
251
+ /***
252
+ * Groups README-promoted API entries by presentation category.
253
+ */
226
254
  function getReadmeGroups(model) {
227
255
  const exportsByName = new Map(model.exports.map((entry) => [entry.name, entry]));
228
256
  const componentNames = new Set(model.components.map((component) => component.name));
@@ -244,22 +272,32 @@ function getReadmeGroups(model) {
244
272
  }
245
273
  return CATEGORY_ORDER.flatMap((title) => {
246
274
  const items = groups.get(title);
247
- if (!items || items.length === 0)
248
- return [];
249
- return [{ title, items: sortReadmeItems(items) }];
275
+ return items === undefined || items.length === 0
276
+ ? []
277
+ : [{ title, items: sortReadmeItems(items) }];
250
278
  });
251
279
  }
280
+ /***
281
+ * Adds one public API item to a README category.
282
+ */
252
283
  function addReadmeItem(groups, title, item) {
253
- const existing = groups.get(title) ?? [];
254
- existing.push(item);
255
- groups.set(title, existing);
284
+ groups.set(title, [...(groups.get(title) ?? []), item]);
256
285
  }
286
+ /***
287
+ * Sorts README category items by symbol name.
288
+ */
257
289
  function sortReadmeItems(items) {
258
290
  return [...items].sort((left, right) => getReadmeItemName(left).localeCompare(getReadmeItemName(right)));
259
291
  }
292
+ /***
293
+ * Returns one README item's source symbol name.
294
+ */
260
295
  function getReadmeItemName(item) {
261
296
  return item.kind === 'component' ? item.component.name : item.exportEntry.name;
262
297
  }
298
+ /***
299
+ * Derives a stable README public API category from a module path.
300
+ */
263
301
  function getReadmeCategory(modulePath, name) {
264
302
  if (modulePath.includes('/config/'))
265
303
  return 'Config';
@@ -281,24 +319,30 @@ function getReadmeCategory(modulePath, name) {
281
319
  return 'Types';
282
320
  return 'Utilities';
283
321
  }
322
+ /***
323
+ * Renders the complete public API reference.
324
+ */
284
325
  function renderExports(model) {
285
326
  const lines = ['# Public API', ''];
286
327
  for (const item of model.exports) {
287
- lines.push(`## ${item.name}`, '');
328
+ lines.push(`## ${item.title ?? item.name}`, '');
329
+ if (item.title !== null && item.title !== item.name)
330
+ lines.push(`Symbol: \`${item.name}\``, '');
288
331
  lines.push(`Kind: \`${item.kind}\``);
289
332
  lines.push(`Module: \`${item.modulePath}\``);
290
333
  lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``, '');
291
334
  if (item.description)
292
335
  lines.push(item.description, '');
336
+ renderReferences(lines, item);
293
337
  renderStructuredRows(lines, item);
294
338
  if (item.signatures.length > 0) {
295
339
  lines.push('### Signatures', '');
296
340
  for (const signature of item.signatures) {
297
341
  lines.push(`- \`${signature.label}\``);
298
342
  for (const parameter of signature.parameters) {
299
- lines.push(` - ${parameter.name}: \`${parameter.type}\`${parameter.required ? '' : ' (optional)'}${parameter.description ? ` — ${parameter.description}` : ''}`);
343
+ lines.push(` - ${parameter.name}: \`${parameter.type}\`${parameter.required ? '' : ' (optional)'}`);
300
344
  }
301
- lines.push(` - returns: \`${signature.returnType ?? 'void'}\`${signature.returnDescription ? ` — ${signature.returnDescription}` : ''}`);
345
+ lines.push(` - returns: \`${signature.returnType ?? 'void'}\``);
302
346
  }
303
347
  lines.push('');
304
348
  }
@@ -314,6 +358,9 @@ function renderExports(model) {
314
358
  }
315
359
  return `${lines.join('\n').trimEnd()}\n`;
316
360
  }
361
+ /***
362
+ * Renders the complete component registry.
363
+ */
317
364
  function renderComponents(model) {
318
365
  const lines = ['# Components', ''];
319
366
  for (const component of model.components) {
@@ -321,6 +368,7 @@ function renderComponents(model) {
321
368
  lines.push(`Source: \`${component.sourceLocation.filePath}:${component.sourceLocation.line}:${component.sourceLocation.column}\``, '');
322
369
  if (component.description)
323
370
  lines.push(component.description, '');
371
+ renderReferences(lines, component);
324
372
  if (component.exportPaths.length > 0) {
325
373
  lines.push(`Export paths: ${component.exportPaths.map((path) => `\`${path}\``).join(', ')}`, '');
326
374
  }
@@ -335,12 +383,21 @@ function renderComponents(model) {
335
383
  }
336
384
  return `${lines.join('\n').trimEnd()}\n`;
337
385
  }
386
+ /***
387
+ * Escapes one Markdown table cell.
388
+ */
338
389
  function escapeTableCell(value) {
339
390
  return value.replaceAll('|', '\\|');
340
391
  }
392
+ /***
393
+ * Renders an optional default value.
394
+ */
341
395
  function renderDefault(value) {
342
396
  return value === undefined ? '—' : `\`${escapeTableCell(value)}\``;
343
397
  }
398
+ /***
399
+ * Flattens nested configuration members into dot paths.
400
+ */
344
401
  function flattenConfigMembers(members, prefix = '') {
345
402
  return members.flatMap((member) => {
346
403
  const path = prefix ? `${prefix}.${member.name}` : member.name;
@@ -355,6 +412,9 @@ function flattenConfigMembers(members, prefix = '') {
355
412
  return [current, ...children];
356
413
  });
357
414
  }
415
+ /***
416
+ * Returns the accessible badge label stored in a rendered badge artifact.
417
+ */
358
418
  function badgeLabel(model, badgePath) {
359
419
  const fileName = badgePath.split('/').pop();
360
420
  if (!fileName)
@@ -363,13 +423,6 @@ function badgeLabel(model, badgePath) {
363
423
  const badge = model.badges.find((entry) => entry.id === id);
364
424
  return badge ? `${badge.label}: ${badge.value}` : badgePath;
365
425
  }
366
- function toFileStem(value) {
367
- return value
368
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
369
- .replace(/[^A-Za-z0-9]+/g, '-')
370
- .replace(/^-+|-+$/g, '')
371
- .toLowerCase();
372
- }
373
426
  const CATEGORY_ORDER = [
374
427
  'Config',
375
428
  'Documentation',
@@ -0,0 +1,2 @@
1
+ /*** Convert a documentation label to the stable lowercase kebab-case file stem used by Paradox render artifacts. */
2
+ export declare function toFileStem(value: string): string;
@@ -0,0 +1,8 @@
1
+ /*** Convert a documentation label to the stable lowercase kebab-case file stem used by Paradox render artifacts. */
2
+ export function toFileStem(value) {
3
+ return value
4
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
5
+ .replace(/[^A-Za-z0-9]+/g, '-')
6
+ .replace(/^-+|-+$/g, '')
7
+ .toLowerCase();
8
+ }