@ankhorage/paradox 0.0.10 → 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/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);
@@ -1,5 +1,7 @@
1
1
  import type { ParadoxConfig } from './types.js';
2
2
  /***
3
3
  * Defines a Paradox configuration object without changing its shape.
4
+ *
5
+ * @readme
4
6
  */
5
7
  export declare function defineParadoxConfig(config: ParadoxConfig): ParadoxConfig;
@@ -1,5 +1,7 @@
1
1
  /***
2
2
  * Defines a Paradox configuration object without changing its shape.
3
+ *
4
+ * @readme
3
5
  */
4
6
  export function defineParadoxConfig(config) {
5
7
  return config;
@@ -2,6 +2,7 @@
2
2
  * Configuration for running Paradox.
3
3
  *
4
4
  * @config
5
+ * @readme
5
6
  */
6
7
  export interface ParadoxConfig {
7
8
  mode?: 'safe' | 'write';
@@ -1,4 +1,9 @@
1
1
  import type { DocumentationModel, ExportKind } from './types.js';
2
+ interface ExampleInput {
3
+ title: string | null;
4
+ language: string | null;
5
+ code: string;
6
+ }
2
7
  interface ExportMemberInput {
3
8
  name: string;
4
9
  kind: 'property' | 'method';
@@ -31,6 +36,8 @@ interface BuildModelInput {
31
36
  exports: {
32
37
  name: string;
33
38
  description: string | null;
39
+ isReadme: boolean;
40
+ examples: ExampleInput[];
34
41
  kind: ExportKind;
35
42
  modulePath: string;
36
43
  sourceLocation: {
@@ -56,6 +63,8 @@ interface BuildModelInput {
56
63
  components: {
57
64
  name: string;
58
65
  description: string | null;
66
+ isReadme: boolean;
67
+ examples: ExampleInput[];
59
68
  modulePath: string;
60
69
  sourceLocation: {
61
70
  filePath: string;
@@ -67,9 +76,18 @@ interface BuildModelInput {
67
76
  name: string;
68
77
  type: string;
69
78
  required: boolean;
79
+ defaultValue?: string;
70
80
  description: string | null;
71
81
  }[];
72
82
  }[];
83
+ sequenceScenarios: {
84
+ kind: 'bin' | 'export';
85
+ name: string;
86
+ sourcePath: string;
87
+ symbolName: string;
88
+ description: string | null;
89
+ isReadme: boolean;
90
+ }[];
73
91
  usage: {
74
92
  packageName: string;
75
93
  commands: {
@@ -79,6 +97,7 @@ interface BuildModelInput {
79
97
  } | null;
80
98
  config: {
81
99
  exportName: string;
100
+ isReadme: boolean;
82
101
  members: ConfigMemberInput[];
83
102
  } | null;
84
103
  entrypoints: string[];
@@ -27,6 +27,7 @@ export function buildModel(analysis) {
27
27
  config: analysis.config !== null
28
28
  ? {
29
29
  exportName: analysis.config.exportName,
30
+ isReadme: analysis.config.isReadme,
30
31
  configFile: getDefaultConfigFileName(analysis.packageId),
31
32
  factoryName: findConfigFactoryName(analysis.config.exportName, [
32
33
  ...exportsByName.keys(),
@@ -45,6 +46,14 @@ export function buildModel(analysis) {
45
46
  .sort((left, right) => left.path.localeCompare(right.path)),
46
47
  exports,
47
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
+ }))),
48
57
  graphs: {
49
58
  imports: [...analysis.graphs.imports],
50
59
  calls: [...analysis.graphs.calls],
@@ -57,6 +66,8 @@ function mapExport(item, exportNames) {
57
66
  return {
58
67
  name: item.name,
59
68
  description: item.description,
69
+ isReadme: item.isReadme,
70
+ examples: item.examples.map((example) => ({ ...example })),
60
71
  kind: item.kind,
61
72
  modulePath: item.modulePath,
62
73
  sourceLocation: {
@@ -95,6 +106,8 @@ function mapComponent(component, exportModel) {
95
106
  return {
96
107
  name: component.name,
97
108
  description: component.description,
109
+ isReadme: component.isReadme,
110
+ examples: component.examples.map((example) => ({ ...example })),
98
111
  modulePath: component.modulePath,
99
112
  sourceLocation: {
100
113
  filePath: component.sourceLocation.filePath,
@@ -106,6 +119,7 @@ function mapComponent(component, exportModel) {
106
119
  name: prop.name,
107
120
  type: prop.type,
108
121
  required: prop.required,
122
+ defaultValue: prop.defaultValue,
109
123
  description: prop.description,
110
124
  }))),
111
125
  };
@@ -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 {
@@ -30,6 +31,7 @@ interface UsageCommandModel {
30
31
  }
31
32
  interface ConfigModel {
32
33
  exportName: string;
34
+ isReadme: boolean;
33
35
  configFile: string;
34
36
  factoryName: string | null;
35
37
  members: ConfigMemberModel[];
@@ -37,6 +39,8 @@ interface ConfigModel {
37
39
  export interface ExportModel {
38
40
  name: string;
39
41
  description: string | null;
42
+ isReadme: boolean;
43
+ examples: ExampleModel[];
40
44
  kind: ExportKind;
41
45
  modulePath: string;
42
46
  sourceLocation: SourceLocationModel;
@@ -49,11 +53,26 @@ export type ExportKind = 'function' | 'type' | 'unknown';
49
53
  export interface ComponentModel {
50
54
  name: string;
51
55
  description: string | null;
56
+ isReadme: boolean;
57
+ examples: ExampleModel[];
52
58
  modulePath: string;
53
59
  sourceLocation: SourceLocationModel;
54
60
  exportPaths: string[];
55
61
  props: PropModel[];
56
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
+ }
71
+ interface ExampleModel {
72
+ title: string | null;
73
+ language: string | null;
74
+ code: string;
75
+ }
57
76
  interface SourceLocationModel {
58
77
  filePath: string;
59
78
  line: number;
@@ -91,6 +110,7 @@ interface PropModel {
91
110
  name: string;
92
111
  type: string;
93
112
  required: boolean;
113
+ defaultValue?: string;
94
114
  description: string | null;
95
115
  }
96
116
  interface ConfigMemberModel {
@@ -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
  }