@ankhorage/paradox 0.1.0 → 0.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 3a7c982: Render sequence diagrams from call flow instead of import topology.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/paradox
5
5
 
6
- ![license: MIT](./paradox/badges/license.svg) ![npm: v0.0.10](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
6
+ ![license: MIT](./paradox/badges/license.svg) ![npm: v0.1.0](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
7
7
 
8
8
  Deterministic documentation generator for TypeScript packages.
9
9
 
@@ -13,6 +13,20 @@ Deterministic documentation generator for TypeScript packages.
13
13
  bunx @ankhorage/paradox
14
14
  ```
15
15
 
16
+ ## CLI
17
+
18
+ ### paradox
19
+
20
+ Runs the Paradox CLI.
21
+
22
+ The command discovers the nearest Paradox config, resolves the package and output roots,
23
+ analyzes the package, builds the documentation model, renders all documentation artifacts,
24
+ and writes them to the configured output directory.
25
+
26
+ ```bash
27
+ bunx @ankhorage/paradox
28
+ ```
29
+
16
30
  ## Documentation Tags
17
31
 
18
32
  <details>
@@ -68,7 +82,6 @@ export default defineParadoxConfig({
68
82
  - [Architecture overview](./paradox/diagrams/architecture-overview.mmd)
69
83
  - [Module relationships](./paradox/diagrams/module-relationships.mmd)
70
84
  - [Export graph](./paradox/diagrams/export-graph.mmd)
71
- - [Entrypoint sequence](./paradox/diagrams/entrypoint-sequence.mmd)
72
85
 
73
86
  ## Architecture preview
74
87
 
@@ -90,6 +103,7 @@ graph TD
90
103
  module_src_analyze_analyze_ts --> module_src_analyze_semantic_createTypeScriptProgram_ts
91
104
  module_src_analyze_analyze_ts --> module_src_analyze_semantic_exports_ts
92
105
  module_src_analyze_analyze_ts --> module_src_analyze_semantic_graphs_ts
106
+ module_src_analyze_analyze_ts --> module_src_analyze_sequenceScenarios_ts
93
107
  module_src_analyze_analyze_ts --> module_src_analyze_types_ts
94
108
  module_src_analyze_analyze_ts --> module_src_analyze_usage_ts
95
109
  module_src_analyze_analyze_ts --> module_src_config_types_ts
@@ -165,6 +179,13 @@ graph TD
165
179
  package__ankhorage_paradox -.-> module_src_analyze_semantic_tagRegistry_ts
166
180
  module_src_analyze_semantic_utils_ts["src/analyze/semantic/utils.ts"]
167
181
  package__ankhorage_paradox -.-> module_src_analyze_semantic_utils_ts
182
+ module_src_analyze_sequenceScenarios_ts["src/analyze/sequenceScenarios.ts"]
183
+ package__ankhorage_paradox -.-> module_src_analyze_sequenceScenarios_ts
184
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_semantic_utils_ts
185
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_types_ts
186
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_usage_ts
187
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_utils_getParadoxComment_ts
188
+ module_src_analyze_sequenceScenarios_ts --> module_src_analyze_utils_parseParadoxComment_ts
168
189
  module_src_analyze_types_ts["src/analyze/types.ts"]
169
190
  package__ankhorage_paradox -.-> module_src_analyze_types_ts
170
191
  module_src_analyze_usage_ts["src/analyze/usage.ts"]
@@ -8,6 +8,7 @@ import { createProject } from './project.js';
8
8
  import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
9
9
  import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
10
10
  import { collectCallGraph, collectComponentCompositionGraph, collectImportGraph, } from './semantic/graphs.js';
11
+ import { analyzeSequenceScenarios } from './sequenceScenarios.js';
11
12
  import { createUsageFromPackageJson } from './usage.js';
12
13
  export async function analyze(config, runtime) {
13
14
  const root = runtime.packageRoot;
@@ -20,6 +21,7 @@ export async function analyze(config, runtime) {
20
21
  const { config: configMetadata, exports } = analyzeExports(project, { root, entrypoints });
21
22
  const components = analyzeComponents(exports, { program });
22
23
  const modules = analyzeModules(project, { root, entrypoints });
24
+ const sequenceScenarios = analyzeSequenceScenarios({ project, root, pkg, exports });
23
25
  const configExport = configMetadata
24
26
  ? (exports.find((entry) => entry.name === configMetadata.exportName) ?? null)
25
27
  : null;
@@ -50,6 +52,7 @@ export async function analyze(config, runtime) {
50
52
  entrypoints: entrypoints.map((entrypoint) => entrypoint.replaceAll('\\', '/')).sort(),
51
53
  modules,
52
54
  badges,
55
+ sequenceScenarios,
53
56
  usage,
54
57
  config: configMetadata
55
58
  ? {
@@ -0,0 +1,14 @@
1
+ import { type Project } from 'ts-morph';
2
+ import type { AnalysisExport, AnalysisSequenceScenario } from './types.js';
3
+ import type { PackageJsonModel } from './usage.js';
4
+ interface AnalyzeSequenceScenariosOptions {
5
+ project: Project;
6
+ root: string;
7
+ pkg: PackageJsonModel;
8
+ exports: readonly AnalysisExport[];
9
+ }
10
+ /***
11
+ * Finds scenario roots that can be rendered as sequence diagrams.
12
+ */
13
+ export declare function analyzeSequenceScenarios({ project, root, pkg, exports, }: AnalyzeSequenceScenariosOptions): AnalysisSequenceScenario[];
14
+ export {};
@@ -0,0 +1,174 @@
1
+ import { isAbsolute, join, normalize } from 'node:path';
2
+ import { Node as MorphNode, } from 'ts-morph';
3
+ import { relativeToRoot, toPosixPath } from './semantic/utils.js';
4
+ import { getParadoxComment } from './utils/getParadoxComment.js';
5
+ import { parseParadoxComment } from './utils/parseParadoxComment.js';
6
+ /***
7
+ * Finds scenario roots that can be rendered as sequence diagrams.
8
+ */
9
+ export function analyzeSequenceScenarios({ project, root, pkg, exports, }) {
10
+ return uniqueScenarios([
11
+ ...analyzeBinSequenceScenarios(project, root, pkg),
12
+ ...analyzeExportSequenceScenarios(exports),
13
+ ]);
14
+ }
15
+ function analyzeBinSequenceScenarios(project, root, pkg) {
16
+ return getBinEntries(pkg).flatMap((entry) => {
17
+ const sourceFile = resolveBinSourceFile(project, root, entry.targetPath);
18
+ if (!sourceFile)
19
+ return [];
20
+ const callableRoot = findTopLevelInvokedLocalCallable(sourceFile);
21
+ if (callableRoot === null)
22
+ return [];
23
+ return [
24
+ {
25
+ kind: 'bin',
26
+ name: entry.name,
27
+ sourcePath: relativeToRoot(root, sourceFile.getFilePath()),
28
+ symbolName: callableRoot.symbolName,
29
+ description: callableRoot.description,
30
+ isReadme: callableRoot.isReadme,
31
+ },
32
+ ];
33
+ });
34
+ }
35
+ function analyzeExportSequenceScenarios(exports) {
36
+ return exports.flatMap((entry) => {
37
+ if (entry.signatures.length === 0)
38
+ return [];
39
+ return [
40
+ {
41
+ kind: 'export',
42
+ name: entry.name,
43
+ sourcePath: entry.modulePath,
44
+ symbolName: entry.name,
45
+ description: entry.description,
46
+ isReadme: entry.isReadme,
47
+ },
48
+ ];
49
+ });
50
+ }
51
+ function getBinEntries(pkg) {
52
+ if (pkg.bin == null)
53
+ return [];
54
+ if (typeof pkg.bin === 'string') {
55
+ return [
56
+ {
57
+ name: getPackageBaseName(pkg.name),
58
+ targetPath: pkg.bin,
59
+ },
60
+ ];
61
+ }
62
+ return Object.entries(pkg.bin)
63
+ .map(([name, targetPath]) => ({ name, targetPath }))
64
+ .sort((left, right) => left.name.localeCompare(right.name));
65
+ }
66
+ function resolveBinSourceFile(project, root, targetPath) {
67
+ for (const candidate of getBinSourceCandidates(targetPath)) {
68
+ const sourceFile = getSourceFileByRelativePath(project, root, candidate);
69
+ if (sourceFile)
70
+ return sourceFile;
71
+ }
72
+ return null;
73
+ }
74
+ function getBinSourceCandidates(targetPath) {
75
+ const normalized = toPosixPath(targetPath).replace(/^\.\//, '');
76
+ const candidates = [];
77
+ if (/^src\/.*\.tsx?$/.test(normalized)) {
78
+ candidates.push(normalized);
79
+ }
80
+ if (/^dist\/.*\.jsx?$/.test(normalized)) {
81
+ candidates.push(normalized.replace(/^dist\//, 'src/').replace(/\.jsx?$/, '.ts'));
82
+ candidates.push(normalized.replace(/^dist\//, 'src/').replace(/\.jsx?$/, '.tsx'));
83
+ }
84
+ if (/\.jsx?$/.test(normalized)) {
85
+ candidates.push(normalized.replace(/\.jsx?$/, '.ts'));
86
+ candidates.push(normalized.replace(/\.jsx?$/, '.tsx'));
87
+ }
88
+ return uniqueSorted(candidates);
89
+ }
90
+ function getSourceFileByRelativePath(project, root, relativePath) {
91
+ const absolutePath = normalize(isAbsolute(relativePath) ? relativePath : join(root, relativePath));
92
+ return project.getSourceFile(absolutePath) ?? null;
93
+ }
94
+ function findTopLevelInvokedLocalCallable(sourceFile) {
95
+ const candidates = [];
96
+ sourceFile.forEachDescendant((node) => {
97
+ if (!MorphNode.isCallExpression(node))
98
+ return;
99
+ if (isInsideCallable(node))
100
+ return;
101
+ const callableDeclaration = getLocalFunctionDeclarationForCall(sourceFile, node);
102
+ if (callableDeclaration !== null) {
103
+ candidates.push(callableDeclaration);
104
+ }
105
+ });
106
+ const uniqueCandidates = uniqueByFunctionName(candidates);
107
+ const declaration = uniqueCandidates.length === 1 ? uniqueCandidates[0] : undefined;
108
+ if (declaration === undefined)
109
+ return null;
110
+ const parsedComment = getParsedParadoxComment(declaration);
111
+ return {
112
+ symbolName: declaration.getName() ?? 'main',
113
+ description: parsedComment.description,
114
+ isReadme: parsedComment.isReadme,
115
+ };
116
+ }
117
+ function getParsedParadoxComment(declaration) {
118
+ const comment = getParadoxComment(declaration);
119
+ if (comment === null) {
120
+ return { description: null, isReadme: false };
121
+ }
122
+ const parsed = parseParadoxComment(comment);
123
+ return {
124
+ description: parsed.description,
125
+ isReadme: parsed.isReadme,
126
+ };
127
+ }
128
+ function getLocalFunctionDeclarationForCall(sourceFile, node) {
129
+ const expression = node.getExpression();
130
+ const symbol = expression.getSymbol() ?? expression.getType().getSymbol();
131
+ if (!symbol)
132
+ return null;
133
+ for (const declaration of symbol.getDeclarations()) {
134
+ if (declaration.getSourceFile().getFilePath() !== sourceFile.getFilePath())
135
+ continue;
136
+ if (!MorphNode.isFunctionDeclaration(declaration))
137
+ continue;
138
+ return declaration;
139
+ }
140
+ return null;
141
+ }
142
+ function isInsideCallable(node) {
143
+ return Boolean(node.getFirstAncestor((candidate) => MorphNode.isFunctionDeclaration(candidate) ||
144
+ MorphNode.isMethodDeclaration(candidate) ||
145
+ MorphNode.isFunctionExpression(candidate) ||
146
+ MorphNode.isArrowFunction(candidate) ||
147
+ MorphNode.isClassDeclaration(candidate)));
148
+ }
149
+ function getPackageBaseName(packageName) {
150
+ return packageName.split('/').pop() ?? packageName;
151
+ }
152
+ function uniqueScenarios(scenarios) {
153
+ const seen = new Set();
154
+ return scenarios.filter((scenario) => {
155
+ const key = `${scenario.kind}:${scenario.name}:${scenario.sourcePath}:${scenario.symbolName}`;
156
+ if (seen.has(key))
157
+ return false;
158
+ seen.add(key);
159
+ return true;
160
+ });
161
+ }
162
+ function uniqueByFunctionName(declarations) {
163
+ const seen = new Set();
164
+ return declarations.filter((declaration) => {
165
+ const name = declaration.getName();
166
+ if (name === undefined || seen.has(name))
167
+ return false;
168
+ seen.add(name);
169
+ return true;
170
+ });
171
+ }
172
+ function uniqueSorted(values) {
173
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
174
+ }
@@ -87,6 +87,14 @@ export interface AnalysisModule {
87
87
  dependencies: string[];
88
88
  exports: string[];
89
89
  }
90
+ export interface AnalysisSequenceScenario {
91
+ kind: 'bin' | 'export';
92
+ name: string;
93
+ sourcePath: string;
94
+ symbolName: string;
95
+ description: string | null;
96
+ isReadme: boolean;
97
+ }
90
98
  interface AnalysisTypeMember {
91
99
  name: string;
92
100
  type: string;
@@ -136,6 +144,7 @@ export interface AnalysisResult {
136
144
  entrypoints: string[];
137
145
  modules: AnalysisModule[];
138
146
  badges: AnalysisBadge[];
147
+ sequenceScenarios: AnalysisSequenceScenario[];
139
148
  usage: AnalysisUsage | null;
140
149
  config: {
141
150
  exportName: string;
package/dist/cli.js CHANGED
@@ -5,6 +5,15 @@ import { buildModel } from './model/buildModel.js';
5
5
  import { findParadoxConfigFile, loadParadoxConfig, resolveOutputRoot, resolvePackageRoot, } from './paths/policy.js';
6
6
  import { render } from './render/render.js';
7
7
  import { write } from './write/write.js';
8
+ /***
9
+ * Runs the Paradox CLI.
10
+ *
11
+ * The command discovers the nearest Paradox config, resolves the package and output roots,
12
+ * analyzes the package, builds the documentation model, renders all documentation artifacts,
13
+ * and writes them to the configured output directory.
14
+ *
15
+ * @readme
16
+ */
8
17
  async function main() {
9
18
  const cwd = process.cwd();
10
19
  const configFilePath = await findParadoxConfigFile(cwd);
@@ -80,6 +80,14 @@ interface BuildModelInput {
80
80
  description: string | null;
81
81
  }[];
82
82
  }[];
83
+ sequenceScenarios: {
84
+ kind: 'bin' | 'export';
85
+ name: string;
86
+ sourcePath: string;
87
+ symbolName: string;
88
+ description: string | null;
89
+ isReadme: boolean;
90
+ }[];
83
91
  usage: {
84
92
  packageName: string;
85
93
  commands: {
@@ -46,6 +46,14 @@ export function buildModel(analysis) {
46
46
  .sort((left, right) => left.path.localeCompare(right.path)),
47
47
  exports,
48
48
  components: sortByName(analysis.components.map((component) => mapComponent(component, exportsByName.get(component.name)))),
49
+ sequenceScenarios: sortByName(analysis.sequenceScenarios.map((scenario) => ({
50
+ kind: scenario.kind,
51
+ name: scenario.name,
52
+ sourcePath: scenario.sourcePath,
53
+ symbolName: scenario.symbolName,
54
+ description: scenario.description,
55
+ isReadme: scenario.isReadme,
56
+ }))),
49
57
  graphs: {
50
58
  imports: [...analysis.graphs.imports],
51
59
  calls: [...analysis.graphs.calls],
@@ -12,6 +12,7 @@ export interface DocumentationModel {
12
12
  modules: ModuleModel[];
13
13
  exports: ExportModel[];
14
14
  components: ComponentModel[];
15
+ sequenceScenarios: SequenceScenarioModel[];
15
16
  graphs: GraphModel;
16
17
  }
17
18
  export interface GeneratedBadge {
@@ -59,6 +60,14 @@ export interface ComponentModel {
59
60
  exportPaths: string[];
60
61
  props: PropModel[];
61
62
  }
63
+ export interface SequenceScenarioModel {
64
+ kind: 'bin' | 'export';
65
+ name: string;
66
+ sourcePath: string;
67
+ symbolName: string;
68
+ description: string | null;
69
+ isReadme: boolean;
70
+ }
62
71
  interface ExampleModel {
63
72
  title: string | null;
64
73
  language: string | null;
@@ -1,3 +1,5 @@
1
+ const MAX_SEQUENCE_CALL_EDGES = 12;
2
+ const MAX_SEQUENCE_PARTICIPANTS = 8;
1
3
  /***
2
4
  * Generates deterministic Mermaid diagrams for the documentation app.
3
5
  */
@@ -18,11 +20,7 @@ export function renderDiagramArtifacts(model) {
18
20
  title: 'Export graph',
19
21
  content: renderExportGraph(model),
20
22
  },
21
- {
22
- path: 'diagrams/entrypoint-sequence.mmd',
23
- title: 'Entrypoint sequence',
24
- content: renderEntrypointSequence(model),
25
- },
23
+ ...renderSequenceArtifacts(model),
26
24
  ];
27
25
  }
28
26
  function renderArchitectureOverview(model) {
@@ -71,34 +69,88 @@ function renderExportGraph(model) {
71
69
  }
72
70
  return `${lines.join('\n')}\n`;
73
71
  }
74
- function renderEntrypointSequence(model) {
72
+ function renderSequenceArtifacts(model) {
73
+ return model.sequenceScenarios.flatMap((scenario) => {
74
+ const content = renderSequenceScenario(model, scenario);
75
+ if (content === null)
76
+ return [];
77
+ return [
78
+ {
79
+ path: `diagrams/sequences/${toFileStem(scenario.name)}.mmd`,
80
+ title: `${scenario.name} sequence`,
81
+ content,
82
+ },
83
+ ];
84
+ });
85
+ }
86
+ function renderSequenceScenario(model, scenario) {
75
87
  const lines = ['sequenceDiagram'];
76
- const callEdges = model.graphs.calls;
77
- if (callEdges.length > 0) {
78
- const participants = new Map();
79
- for (const edge of callEdges) {
80
- participants.set(edge.fromSymbol, `participant ${toMermaidId(`participant-${edge.fromSymbol}`)} as ${edge.fromSymbol}`);
81
- participants.set(edge.toSymbol, `participant ${toMermaidId(`participant-${edge.toSymbol}`)} as ${edge.toSymbol}`);
82
- }
83
- lines.push(...participants.values());
84
- lines.push(...callEdges.map((edge) => ` ${toMermaidId(`participant-${edge.fromSymbol}`)}->>${toMermaidId(`participant-${edge.toSymbol}`)}: ${edge.callExpression}`));
85
- return `${lines.join('\n')}\n`;
88
+ const reachableEdges = collectReachableCallEdges(model.graphs.calls, scenario.symbolName);
89
+ const participants = collectSequenceParticipants(reachableEdges);
90
+ if (reachableEdges.length === 0)
91
+ return null;
92
+ if (reachableEdges.length > MAX_SEQUENCE_CALL_EDGES ||
93
+ participants.length > MAX_SEQUENCE_PARTICIPANTS) {
94
+ return null;
86
95
  }
87
- const participants = new Map();
88
- for (const module of model.modules) {
89
- participants.set(module.path, `participant ${toMermaidId(`participant-${module.path}`)} as ${module.path}`);
96
+ for (const participant of participants) {
97
+ lines.push(` participant ${toMermaidId(`participant-${participant}`)} as ${escapeLabel(participant)}`);
90
98
  }
91
- lines.push(...participants.values());
92
- const interactions = model.modules.flatMap((module) => module.dependencies.map((dependency) => ` ${toMermaidId(`participant-${module.path}`)}->>${toMermaidId(`participant-${dependency}`)}: imports`));
93
- if (interactions.length === 0) {
94
- const packageId = toMermaidId(`participant-${model.packageId}`);
95
- lines.push(` participant ${packageId} as ${model.packageName}`);
96
- lines.push(` Note over ${packageId}: No internal module relationships detected.`);
99
+ renderCallFlow(lines, reachableEdges, scenario.symbolName);
100
+ return `${lines.join('\n')}\n`;
101
+ }
102
+ function collectReachableCallEdges(callEdges, root) {
103
+ const outgoing = groupCallsBySource(callEdges);
104
+ const visited = new Set();
105
+ const ordered = [];
106
+ visitCallEdges(root, outgoing, visited, ordered);
107
+ return ordered;
108
+ }
109
+ function visitCallEdges(fromSymbol, outgoing, visited, ordered) {
110
+ for (const edge of outgoing.get(fromSymbol) ?? []) {
111
+ const key = getCallEdgeKey(edge);
112
+ if (visited.has(key))
113
+ continue;
114
+ visited.add(key);
115
+ ordered.push(edge);
116
+ visitCallEdges(edge.toSymbol, outgoing, visited, ordered);
97
117
  }
98
- else {
99
- lines.push(...interactions);
118
+ }
119
+ function renderCallFlow(lines, callEdges, root) {
120
+ const outgoing = groupCallsBySource(callEdges);
121
+ const visited = new Set();
122
+ renderNestedCalls(lines, root, outgoing, visited);
123
+ }
124
+ function renderNestedCalls(lines, fromSymbol, outgoing, visited) {
125
+ for (const edge of outgoing.get(fromSymbol) ?? []) {
126
+ const key = getCallEdgeKey(edge);
127
+ if (visited.has(key))
128
+ continue;
129
+ visited.add(key);
130
+ const fromId = toMermaidId(`participant-${edge.fromSymbol}`);
131
+ const toId = toMermaidId(`participant-${edge.toSymbol}`);
132
+ lines.push(` ${fromId}->>${toId}: ${formatCallLabel(edge.callExpression)}`);
133
+ renderNestedCalls(lines, edge.toSymbol, outgoing, visited);
134
+ lines.push(` ${toId}-->>${fromId}: return`);
100
135
  }
101
- return `${lines.join('\n')}\n`;
136
+ }
137
+ function groupCallsBySource(callEdges) {
138
+ const grouped = new Map();
139
+ for (const edge of callEdges) {
140
+ const existing = grouped.get(edge.fromSymbol) ?? [];
141
+ existing.push(edge);
142
+ grouped.set(edge.fromSymbol, existing);
143
+ }
144
+ return grouped;
145
+ }
146
+ function collectSequenceParticipants(callEdges) {
147
+ return uniqueSorted(callEdges.flatMap((edge) => [edge.fromSymbol, edge.toSymbol]));
148
+ }
149
+ function getCallEdgeKey(edge) {
150
+ return `${edge.fromSymbol}->${edge.toSymbol}@${edge.sourcePath}:${edge.callExpression}`;
151
+ }
152
+ function formatCallLabel(callExpression) {
153
+ return callExpression.endsWith(')') ? callExpression : `${callExpression}()`;
102
154
  }
103
155
  function renderFallbackEdge(modules, prefix) {
104
156
  if (modules.length === 0) {
@@ -109,6 +161,16 @@ function renderFallbackEdge(modules, prefix) {
109
161
  return ` ${toMermaidId(`${prefix}-${previous.path}`)} -.-> ${toMermaidId(`${prefix}-${module.path}`)}`;
110
162
  });
111
163
  }
164
+ function uniqueSorted(values) {
165
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
166
+ }
167
+ function toFileStem(value) {
168
+ return value
169
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
170
+ .replace(/[^A-Za-z0-9]+/g, '-')
171
+ .replace(/^-+|-+$/g, '')
172
+ .toLowerCase();
173
+ }
112
174
  function toMermaidId(value) {
113
175
  return value.replace(/[^A-Za-z0-9_]/g, '_');
114
176
  }
@@ -32,6 +32,7 @@ function renderReadme(model, outputDir, badges, diagrams) {
32
32
  }
33
33
  lines.push('```', '');
34
34
  }
35
+ renderCliScenarios(lines, model, outputDir, diagrams);
35
36
  renderDocumentationTags(lines);
36
37
  if (model.config?.isReadme) {
37
38
  renderConfiguration(lines, model);
@@ -42,6 +43,37 @@ function renderReadme(model, outputDir, badges, diagrams) {
42
43
  renderReadmeApi(lines, model);
43
44
  return `${lines.join('\n').trimEnd()}\n`;
44
45
  }
46
+ function renderCliScenarios(lines, model, outputDir, diagrams) {
47
+ const scenarios = model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
48
+ if (scenarios.length === 0)
49
+ return;
50
+ lines.push('## CLI', '');
51
+ for (const scenario of scenarios) {
52
+ lines.push(`### ${scenario.name}`, '');
53
+ if (scenario.description !== null) {
54
+ lines.push(scenario.description, '');
55
+ }
56
+ const command = model.usage?.commands.find((item) => item.name === scenario.name);
57
+ if (command !== undefined) {
58
+ lines.push('```bash');
59
+ lines.push(command.command);
60
+ lines.push('```', '');
61
+ }
62
+ const diagram = findScenarioDiagram(diagrams, scenario);
63
+ if (diagram !== undefined) {
64
+ lines.push('<details>');
65
+ lines.push(`<summary>${scenario.name} sequence</summary>`, '');
66
+ lines.push(`Diagram: [${diagram.title}](./${outputDir}/${diagram.path})`, '');
67
+ lines.push('```mermaid');
68
+ lines.push(diagram.content.trimEnd());
69
+ lines.push('```', '');
70
+ lines.push('</details>', '');
71
+ }
72
+ }
73
+ }
74
+ function findScenarioDiagram(diagrams, scenario) {
75
+ return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
76
+ }
45
77
  function renderDocumentationTags(lines) {
46
78
  lines.push('## Documentation Tags', '');
47
79
  for (const tag of DOCUMENTATION_TAGS) {
@@ -325,6 +357,13 @@ function badgeLabel(model, badgePath) {
325
357
  const badge = model.badges.find((entry) => entry.id === id);
326
358
  return badge ? `${badge.label}: ${badge.value}` : badgePath;
327
359
  }
360
+ function toFileStem(value) {
361
+ return value
362
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
363
+ .replace(/[^A-Za-z0-9]+/g, '-')
364
+ .replace(/^-+|-+$/g, '')
365
+ .toLowerCase();
366
+ }
328
367
  const CATEGORY_ORDER = [
329
368
  'Configuration',
330
369
  'Primitives',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {