@ankhorage/paradox 0.1.27 → 0.2.1

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 (60) hide show
  1. package/CHANGELOG.md +14 -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 +17 -15
  18. package/dist/analyze/modules.js +3 -8
  19. package/dist/analyze/readmeConfig.d.ts +1 -3
  20. package/dist/analyze/readmeConfig.js +8 -19
  21. package/dist/analyze/readmeUsage.d.ts +7 -10
  22. package/dist/analyze/readmeUsage.js +124 -48
  23. package/dist/analyze/semantic/docBlocks.js +18 -48
  24. package/dist/analyze/semantic/exports.js +3 -7
  25. package/dist/analyze/semantic/model.d.ts +0 -2
  26. package/dist/analyze/semantic/paradoxComment.d.ts +1 -11
  27. package/dist/analyze/semantic/paradoxComment.js +1 -43
  28. package/dist/analyze/semantic/tagRegistry.js +2 -1
  29. package/dist/analyze/sequenceScenarios.js +2 -4
  30. package/dist/analyze/sourceFunctions.js +4 -4
  31. package/dist/analyze/types.d.ts +31 -24
  32. package/dist/analyze/usage.d.ts +2 -2
  33. package/dist/analyze/usage.js +3 -33
  34. package/dist/analyze/utils/getExportMetadata.js +11 -40
  35. package/dist/analyze/utils/parseParadoxComment.d.ts +12 -9
  36. package/dist/analyze/utils/parseParadoxComment.js +66 -78
  37. package/dist/cli/index.d.ts +3 -2
  38. package/dist/cli/index.js +3 -2
  39. package/dist/cli/standalone.js +11 -0
  40. package/dist/config/defineParadoxConfig.d.ts +1 -1
  41. package/dist/doc-tags/registry.d.ts +28 -32
  42. package/dist/doc-tags/registry.js +35 -39
  43. package/dist/index.d.ts +1 -1
  44. package/dist/model/buildModel.d.ts +26 -19
  45. package/dist/model/buildModel.js +33 -99
  46. package/dist/model/types.d.ts +27 -20
  47. package/dist/paths/policy.d.ts +1 -1
  48. package/dist/render/renderers/diagrams.js +3 -11
  49. package/dist/render/renderers/html.js +89 -58
  50. package/dist/render/renderers/markdown.js +139 -86
  51. package/dist/render/toFileStem.d.ts +2 -0
  52. package/dist/render/toFileStem.js +8 -0
  53. package/dist/{config/types.d.ts → types/config.d.ts} +3 -5
  54. package/dist/write/write.d.ts +1 -1
  55. package/package.json +2 -1
  56. package/dist/analyze/readmeCli.d.ts +0 -9
  57. package/dist/analyze/readmeCli.js +0 -33
  58. package/dist/analyze/utils/getLeadingParadoxComment.d.ts +0 -10
  59. package/dist/analyze/utils/getLeadingParadoxComment.js +0 -16
  60. /package/dist/{config/types.js → types/config.js} +0 -0
@@ -4,87 +4,45 @@
4
4
  export function buildModel(analysis) {
5
5
  const exportNames = new Set(analysis.exports.map((item) => item.name));
6
6
  const exportsByName = new Map(analysis.exports.map((item) => [item.name, mapExport(item, exportNames)]));
7
- const exports = sortByName([...exportsByName.values()]);
8
7
  return {
9
8
  packageName: analysis.packageName,
10
9
  packageId: analysis.packageId,
11
10
  description: analysis.description,
12
11
  collaborators: analysis.collaborators,
13
12
  donation: analysis.donation === null ? null : { account: analysis.donation.account },
14
- badges: analysis.badges.map((badge) => ({
15
- id: badge.id,
16
- label: badge.label,
17
- value: badge.value,
18
- color: badge.color,
19
- })),
20
- usage: analysis.usage !== null
21
- ? {
22
- packageName: analysis.usage.packageName,
23
- commands: sortByName(analysis.usage.commands.map((command) => ({
24
- name: command.name,
25
- command: command.command,
26
- }))),
27
- }
28
- : null,
29
- readmeUsageDescription: analysis.readmeUsageDescription,
30
- readmeUsage: analysis.readmeUsage
31
- .map((usageEntry) => ({
32
- title: usageEntry.title,
33
- description: usageEntry.description,
34
- language: usageEntry.language,
35
- code: usageEntry.code,
36
- sourcePath: usageEntry.sourcePath,
37
- }))
13
+ badges: analysis.badges.map((badge) => ({ ...badge })),
14
+ usage: { ...analysis.usage },
15
+ usageEntries: analysis.usageEntries
16
+ .map((entry) => ({ ...entry, see: [...entry.see], security: [...entry.security] }))
38
17
  .sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
39
- readmeCli: analysis.readmeCli !== null
40
- ? {
41
- description: analysis.readmeCli.description,
42
- sourcePath: analysis.readmeCli.sourcePath,
43
- }
44
- : null,
45
- readmeConfig: analysis.readmeConfig !== null
46
- ? {
47
- description: analysis.readmeConfig.description,
48
- language: analysis.readmeConfig.language,
49
- code: analysis.readmeConfig.code,
50
- sourcePath: analysis.readmeConfig.sourcePath,
51
- }
52
- : null,
53
- config: analysis.config !== null
54
- ? {
55
- exportName: analysis.config.exportName,
56
- isReadme: analysis.config.isReadme,
18
+ exampleCount: analysis.exampleCount,
19
+ findings: analysis.findings.map((finding) => ({ ...finding })),
20
+ readmeConfig: analysis.readmeConfig === null ? null : { ...analysis.readmeConfig },
21
+ config: analysis.config === null
22
+ ? null
23
+ : {
24
+ ...analysis.config,
25
+ see: [...analysis.config.see],
26
+ security: [...analysis.config.security],
57
27
  members: analysis.config.members,
58
- }
59
- : null,
28
+ },
60
29
  entrypoints: [...analysis.entrypoints].sort((a, b) => a.localeCompare(b)),
61
30
  modules: [...analysis.modules]
62
31
  .map((module) => ({
63
- path: module.path,
64
- isEntrypoint: module.isEntrypoint,
32
+ ...module,
65
33
  dependencies: [...module.dependencies].sort((a, b) => a.localeCompare(b)),
66
34
  exports: [...module.exports].sort((a, b) => a.localeCompare(b)),
67
35
  }))
68
36
  .sort((left, right) => left.path.localeCompare(right.path)),
69
- exports,
37
+ exports: sortByName([...exportsByName.values()]),
70
38
  components: sortByName(analysis.components.map((component) => mapComponent(component, exportsByName.get(component.name)))),
71
39
  sourceFunctions: analysis.sourceFunctions.map((sourceFunction) => ({
72
- name: sourceFunction.name,
73
- description: sourceFunction.description,
74
- sourceLocation: {
75
- filePath: sourceFunction.sourceLocation.filePath,
76
- line: sourceFunction.sourceLocation.line,
77
- column: sourceFunction.sourceLocation.column,
78
- },
40
+ ...sourceFunction,
41
+ see: [...sourceFunction.see],
42
+ security: [...sourceFunction.security],
43
+ sourceLocation: { ...sourceFunction.sourceLocation },
79
44
  })),
80
- sequenceScenarios: sortByName(analysis.sequenceScenarios.map((scenario) => ({
81
- kind: scenario.kind,
82
- name: scenario.name,
83
- sourcePath: scenario.sourcePath,
84
- symbolName: scenario.symbolName,
85
- description: scenario.description,
86
- isReadme: scenario.isReadme,
87
- }))),
45
+ sequenceScenarios: sortByName(analysis.sequenceScenarios.map((scenario) => ({ ...scenario }))),
88
46
  graphs: {
89
47
  imports: [...analysis.graphs.imports],
90
48
  calls: [...analysis.graphs.calls],
@@ -99,39 +57,24 @@ export function buildModel(analysis) {
99
57
  function mapExport(item, exportNames) {
100
58
  return {
101
59
  name: item.name,
60
+ title: item.title,
102
61
  description: item.description,
103
62
  isReadme: item.isReadme,
104
- examples: item.examples.map((example) => ({ ...example })),
63
+ see: [...item.see],
64
+ security: [...item.security],
105
65
  kind: item.kind,
106
66
  modulePath: item.modulePath,
107
- sourceLocation: {
108
- filePath: item.sourceLocation.filePath,
109
- line: item.sourceLocation.line,
110
- column: item.sourceLocation.column,
111
- },
67
+ sourceLocation: { ...item.sourceLocation },
112
68
  exportPaths: [...item.exportPaths].sort((a, b) => a.localeCompare(b)),
113
69
  relatedSymbols: item.relatedSymbols
114
70
  .filter((symbol) => exportNames.has(symbol))
115
71
  .sort((a, b) => a.localeCompare(b)),
116
72
  signatures: item.signatures.map((signature) => ({
117
- label: signature.label,
118
- parameters: sortByName(signature.parameters.map((parameter) => ({
119
- name: parameter.name,
120
- type: parameter.type,
121
- required: parameter.required,
122
- description: parameter.description,
123
- }))),
124
- returnType: signature.returnType,
125
- returnDescription: signature.returnDescription,
73
+ ...signature,
74
+ parameters: sortByName(signature.parameters.map((parameter) => ({ ...parameter }))),
126
75
  })),
127
76
  members: sortByName(item.members.map((member) => ({
128
- name: member.name,
129
- kind: member.kind,
130
- type: member.type,
131
- required: member.required,
132
- description: member.description,
133
- defaultValue: member.defaultValue,
134
- inheritedFrom: member.inheritedFrom,
77
+ ...member,
135
78
  children: member.children,
136
79
  }))),
137
80
  structuredRows: item.structuredRows.map((row) => ({ values: { ...row.values } })),
@@ -145,25 +88,16 @@ function mapComponent(component, exportModel) {
145
88
  name: component.name,
146
89
  description: component.description,
147
90
  isReadme: component.isReadme,
148
- examples: component.examples.map((example) => ({ ...example })),
91
+ see: [...component.see],
92
+ security: [...component.security],
149
93
  modulePath: component.modulePath,
150
- sourceLocation: {
151
- filePath: component.sourceLocation.filePath,
152
- line: component.sourceLocation.line,
153
- column: component.sourceLocation.column,
154
- },
94
+ sourceLocation: { ...component.sourceLocation },
155
95
  exportPaths: exportModel?.exportPaths ?? [...component.exportPaths].sort((a, b) => a.localeCompare(b)),
156
- props: sortByName(component.props.map((prop) => ({
157
- name: prop.name,
158
- type: prop.type,
159
- required: prop.required,
160
- defaultValue: prop.defaultValue,
161
- description: prop.description,
162
- }))),
96
+ props: sortByName(component.props.map((prop) => ({ ...prop }))),
163
97
  };
164
98
  }
165
99
  /***
166
- * Returns a copy of items sorted by their `name` property.
100
+ * Returns a copy of items sorted by their name property.
167
101
  */
168
102
  function sortByName(items) {
169
103
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
@@ -1,3 +1,4 @@
1
+ import type { PolicySeverity } from '@ankhorage/policy/status';
1
2
  /***
2
3
  * Serializable model consumed by renderers and writers.
3
4
  */
@@ -8,10 +9,10 @@ export interface DocumentationModel {
8
9
  collaborators: true | null;
9
10
  donation: DonationModel | null;
10
11
  badges: GeneratedBadge[];
11
- usage: UsageModel | null;
12
- readmeUsageDescription: string | null;
13
- readmeUsage: ReadmeUsageModel[];
14
- readmeCli: ReadmeCliModel | null;
12
+ usage: UsageModel;
13
+ usageEntries: UsageEntryModel[];
14
+ exampleCount: number;
15
+ findings: DocumentationFindingModel[];
15
16
  readmeConfig: ReadmeConfigModel | null;
16
17
  config: ConfigModel | null;
17
18
  entrypoints: string[];
@@ -33,39 +34,47 @@ export interface GeneratedBadge {
33
34
  }
34
35
  interface UsageModel {
35
36
  packageName: string;
36
- commands: UsageCommandModel[];
37
- }
38
- interface UsageCommandModel {
39
- name: string;
40
37
  command: string;
41
38
  }
42
- interface ReadmeUsageModel {
39
+ interface UsageEntryModel {
40
+ area: 'cli' | 'examples';
43
41
  title: string | null;
44
42
  description: string | null;
45
43
  language: string;
46
44
  code: string;
47
45
  sourcePath: string;
46
+ isReadme: boolean;
47
+ see: string[];
48
+ security: string[];
48
49
  }
49
- interface ReadmeCliModel {
50
- description: string | null;
51
- sourcePath: string;
50
+ interface DocumentationFindingModel {
51
+ ruleId: string;
52
+ severity: PolicySeverity;
53
+ message: string;
54
+ sourcePath: string | null;
55
+ line: number | null;
52
56
  }
53
57
  interface ReadmeConfigModel {
54
- description: string | null;
55
58
  language: string;
56
59
  code: string;
57
60
  sourcePath: string;
58
61
  }
59
62
  interface ConfigModel {
60
63
  exportName: string;
64
+ title: string | null;
65
+ description: string | null;
61
66
  isReadme: boolean;
67
+ see: string[];
68
+ security: string[];
62
69
  members: ConfigMemberModel[];
63
70
  }
64
71
  export interface ExportModel {
65
72
  name: string;
73
+ title: string | null;
66
74
  description: string | null;
67
75
  isReadme: boolean;
68
- examples: ExampleModel[];
76
+ see: string[];
77
+ security: string[];
69
78
  kind: ExportKind;
70
79
  modulePath: string;
71
80
  sourceLocation: SourceLocationModel;
@@ -80,7 +89,8 @@ export interface ComponentModel {
80
89
  name: string;
81
90
  description: string | null;
82
91
  isReadme: boolean;
83
- examples: ExampleModel[];
92
+ see: string[];
93
+ security: string[];
84
94
  modulePath: string;
85
95
  sourceLocation: SourceLocationModel;
86
96
  exportPaths: string[];
@@ -89,6 +99,8 @@ export interface ComponentModel {
89
99
  interface SourceFunctionModel {
90
100
  name: string;
91
101
  description: string | null;
102
+ see: string[];
103
+ security: string[];
92
104
  sourceLocation: SourceLocationModel;
93
105
  }
94
106
  export interface SequenceScenarioModel {
@@ -105,11 +117,6 @@ export interface ModuleModel {
105
117
  dependencies: string[];
106
118
  exports: string[];
107
119
  }
108
- interface ExampleModel {
109
- title: string | null;
110
- language: string | null;
111
- code: string;
112
- }
113
120
  interface SourceLocationModel {
114
121
  filePath: string;
115
122
  line: number;
@@ -1,4 +1,4 @@
1
- import type { ParadoxConfig } from '../config/types.js';
1
+ import type { ParadoxConfig } from '../types/config.js';
2
2
  /***
3
3
  * Searches upward from a start directory until it finds a supported Paradox config file.
4
4
  */
@@ -1,3 +1,5 @@
1
+ import { uniqueSortedStrings } from '@ankhorage/utility/array';
2
+ import { toFileStem } from '../toFileStem.js';
1
3
  const MAX_SEQUENCE_CALL_EDGES = 12;
2
4
  const MAX_SEQUENCE_PARTICIPANTS = 8;
3
5
  const MAX_BIN_SEQUENCE_PARTICIPANTS = 12;
@@ -173,7 +175,7 @@ function groupCallsBySource(callEdges) {
173
175
  return grouped;
174
176
  }
175
177
  function collectSequenceParticipants(callEdges) {
176
- return uniqueSorted(callEdges.flatMap((edge) => [edge.fromSymbol, edge.toSymbol]));
178
+ return uniqueSortedStrings(callEdges.flatMap((edge) => [edge.fromSymbol, edge.toSymbol]));
177
179
  }
178
180
  function getCallEdgeKey(edge) {
179
181
  return `${edge.fromSymbol}->${edge.toSymbol}@${edge.sourcePath}:${edge.callExpression}`;
@@ -190,16 +192,6 @@ function renderFallbackEdge(modules, prefix) {
190
192
  return ` ${toMermaidId(`${prefix}-${previous.path}`)} -.-> ${toMermaidId(`${prefix}-${module.path}`)}`;
191
193
  });
192
194
  }
193
- function uniqueSorted(values) {
194
- return [...new Set(values)].sort((left, right) => left.localeCompare(right));
195
- }
196
- function toFileStem(value) {
197
- return value
198
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
199
- .replace(/[^A-Za-z0-9]+/g, '-')
200
- .replace(/^-+|-+$/g, '')
201
- .toLowerCase();
202
- }
203
195
  function toMermaidId(value) {
204
196
  return value.replace(/[^A-Za-z0-9_]/g, '_');
205
197
  }
@@ -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
  */