@fluentcommerce/ai-skills 0.13.0 → 0.15.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 (97) hide show
  1. package/README.md +17 -12
  2. package/bin/cli.mjs +219 -43
  3. package/content/cli/skills/fluent-bootstrap/SKILL.md +1 -1
  4. package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +1 -1
  5. package/content/cli/skills/fluent-cli-reference/SKILL.md +1 -1
  6. package/content/cli/skills/fluent-cli-retailer/SKILL.md +1 -1
  7. package/content/cli/skills/fluent-connect/SKILL.md +58 -3
  8. package/content/cli/skills/fluent-profile/SKILL.md +35 -5
  9. package/content/cli/skills/fluent-workflow/SKILL.md +2 -1
  10. package/content/dev/agents/fluent-backend-dev.md +2 -2
  11. package/content/dev/agents/fluent-dev.md +1 -1
  12. package/content/dev/agents/fluent-frontend-dev.md +1 -1
  13. package/content/dev/skills/fluent-account-snapshot/SKILL.md +1 -1
  14. package/content/dev/skills/fluent-archive/SKILL.md +2 -1
  15. package/content/dev/skills/fluent-build/SKILL.md +2 -2
  16. package/content/dev/skills/fluent-connection-analysis/SKILL.md +2 -1
  17. package/content/dev/skills/fluent-custom-code/SKILL.md +3 -2
  18. package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +7 -6
  19. package/content/dev/skills/fluent-e2e-test/SKILL.md +1 -1
  20. package/content/dev/skills/fluent-entity-flow-diagnose/SKILL.md +2 -1
  21. package/content/dev/skills/fluent-event-api/SKILL.md +3 -2
  22. package/content/dev/skills/fluent-feature-explain/SKILL.md +2 -1
  23. package/content/dev/skills/fluent-feature-plan/SKILL.md +2 -1
  24. package/content/dev/skills/fluent-feature-status/SKILL.md +1 -1
  25. package/content/dev/skills/fluent-frontend-build/SKILL.md +1 -1
  26. package/content/dev/skills/fluent-frontend-readme/SKILL.md +2 -1
  27. package/content/dev/skills/fluent-frontend-review/SKILL.md +2 -1
  28. package/content/dev/skills/fluent-goal/SKILL.md +1 -1
  29. package/content/dev/skills/fluent-implementation-map/SKILL.md +1 -1
  30. package/content/dev/skills/fluent-inventory-catalog/SKILL.md +2 -2
  31. package/content/dev/skills/fluent-job-batch/SKILL.md +6 -3
  32. package/content/dev/skills/fluent-knowledge-init/SKILL.md +1 -1
  33. package/content/dev/skills/{fluent-source-onboard → fluent-module-convert}/SKILL.md +223 -24
  34. package/content/dev/skills/fluent-module-scaffold/SKILL.md +2 -1
  35. package/content/dev/skills/fluent-module-validate/SKILL.md +8 -7
  36. package/content/dev/skills/fluent-mystique-assess/SKILL.md +22 -6
  37. package/content/dev/skills/fluent-mystique-builder/SKILL.md +33 -2
  38. package/content/dev/skills/fluent-mystique-component/SKILL.md +1 -1
  39. package/content/dev/skills/fluent-mystique-diff/SKILL.md +1 -1
  40. package/content/dev/skills/fluent-mystique-preview/SKILL.md +19 -2
  41. package/content/dev/skills/fluent-mystique-scaffold/SDK_REFERENCE.md +2 -2
  42. package/content/dev/skills/fluent-mystique-scaffold/SKILL.md +13 -2
  43. package/content/dev/skills/fluent-mystique-scaffold/TEMPLATES.md +1 -1
  44. package/content/dev/skills/fluent-mystique-sdk-reference/SKILL.md +2 -1
  45. package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +3 -3
  46. package/content/dev/skills/fluent-retailer-config/SKILL.md +3 -3
  47. package/content/dev/skills/fluent-rollback/SKILL.md +1 -1
  48. package/content/dev/skills/fluent-rule-lookup/SKILL.md +2 -1
  49. package/content/dev/skills/fluent-rule-scaffold/SKILL.md +7 -6
  50. package/content/dev/skills/fluent-rule-test/SKILL.md +2 -1
  51. package/content/dev/skills/fluent-scope-plan/SKILL.md +2 -2
  52. package/content/dev/skills/fluent-session/SKILL.md +1 -1
  53. package/content/dev/skills/fluent-settings/SKILL.md +2 -2
  54. package/content/dev/skills/fluent-skill-observability/SKILL.md +1 -1
  55. package/content/dev/skills/fluent-sourcing/SKILL.md +38 -13
  56. package/content/dev/skills/fluent-system-monitoring/SKILL.md +1 -1
  57. package/content/dev/skills/fluent-test-data/SKILL.md +1 -1
  58. package/content/dev/skills/fluent-trace/SKILL.md +3 -2
  59. package/content/dev/skills/fluent-transition-api/SKILL.md +3 -2
  60. package/content/dev/skills/fluent-ui-record/SKILL.md +1 -1
  61. package/content/dev/skills/fluent-ui-test/SKILL.md +9 -8
  62. package/content/dev/skills/fluent-use-case-discover/SKILL.md +2 -2
  63. package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +3 -2
  64. package/content/dev/skills/fluent-workspace-tree/SKILL.md +4 -3
  65. package/content/knowledge/index.md +3 -3
  66. package/content/knowledge/platform/domain-model.md +1 -0
  67. package/content/knowledge/platform/module-structure.md +5 -5
  68. package/content/knowledge/platform/mystique-routing.md +6 -3
  69. package/content/knowledge/platform/permissions-and-contexts.md +2 -2
  70. package/content/knowledge/platform/rule-test-patterns.md +1 -1
  71. package/content/knowledge/platform/workflow-json-structure.md +1 -1
  72. package/content/mcp-extn/skills/fluent-mcp-core/SKILL.md +2 -1
  73. package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +3 -2
  74. package/content/rfl/skills/fluent-rfl-assess/SKILL.md +1 -1
  75. package/docs/01-first-session.md +175 -0
  76. package/docs/02-prompt-guide.md +246 -0
  77. package/docs/03-use-cases.md +1181 -0
  78. package/docs/04-onboarding-plan.md +355 -0
  79. package/docs/05-getting-started.md +262 -0
  80. package/docs/06-dev-workflow.md +1040 -0
  81. package/docs/INDEX.md +40 -0
  82. package/docs/agents-and-skills-guide.md +199 -0
  83. package/docs/capability-map.md +165 -0
  84. package/docs/chrome-devtools-mcp-reference.md +401 -0
  85. package/docs/fluent-ai-skills-reference.md +1351 -0
  86. package/docs/manifest-safety.md +79 -0
  87. package/docs/mcp-servers.md +209 -0
  88. package/docs/workflow-reference.md +167 -0
  89. package/lib/fluent-brand.css +55 -0
  90. package/metadata.json +7 -6
  91. package/package.json +17 -3
  92. package/scripts/postinstall.mjs +38 -0
  93. package/{content/dev/skills/fluent-trace/scripts/analyze-event-capture.mjs → tools/event-capture-analyzer.mjs} +3 -3
  94. package/tools/{generate-feature-dashboard.mjs → feature-dashboard.mjs} +2 -2
  95. package/tools/manifest-diff.mjs +1 -1
  96. package/{content/dev/skills/fluent-mystique-assess/validator.mjs → tools/manifest-validator.mjs} +2 -2
  97. package/tools/workflow-explainer.mjs +1021 -0
@@ -0,0 +1,1021 @@
1
+ #!/usr/bin/env node
2
+ // fluent-ai-skills/tools/workflow-explainer.mjs
3
+ // Workflow Explainer — Local workflow analysis, custom rule source extraction, Mermaid visualization
4
+ // Zero npm dependencies — uses only Node.js built-ins
5
+ //
6
+ // Usage:
7
+ // node tools/workflow-explainer.mjs --workflow <path-to-json> [--workspace <path>] [--profile <NAME>] [--output <path>] [--html]
8
+ //
9
+ // Examples:
10
+ // node tools/workflow-explainer.mjs --workflow accounts/SAGIRISH/workflows/Module\ Test/ORDER-MULTI.json --profile SAGIRISH
11
+ // node tools/workflow-explainer.mjs --workflow ORDER-MULTI.json --workspace /path/to/fluent-ai-workspace --profile SAGIRISH --output report.md
12
+ // node tools/workflow-explainer.mjs --workflow ORDER-MULTI.json --profile SAGIRISH --html --output report.html
13
+
14
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
15
+ import { join, basename, dirname, resolve, extname, relative } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+
21
+ // ─── CLI Argument Parsing ────────────────────────────────────────────────────
22
+
23
+ function parseArgs() {
24
+ const args = process.argv.slice(2);
25
+ const opts = { workflow: null, workspace: null, profile: null, output: null, html: false };
26
+ for (let i = 0; i < args.length; i++) {
27
+ switch (args[i]) {
28
+ case '--workflow': case '-w': opts.workflow = args[++i]; break;
29
+ case '--workspace': opts.workspace = args[++i]; break;
30
+ case '--profile': case '-p': opts.profile = args[++i]; break;
31
+ case '--output': case '-o': opts.output = args[++i]; break;
32
+ case '--html': opts.html = true; break;
33
+ case '--help': case '-h': printHelp(); process.exit(0);
34
+ default:
35
+ if (!args[i].startsWith('-') && !opts.workflow) opts.workflow = args[i];
36
+ else if (!args[i].startsWith('-')) { console.error(`Unknown argument: ${args[i]}`); process.exit(1); }
37
+ else { console.error(`Unknown option: ${args[i]}`); process.exit(1); }
38
+ }
39
+ }
40
+ if (!opts.workflow) { console.error('Error: --workflow <path> is required\n'); printHelp(); process.exit(1); }
41
+ if (!opts.workspace) opts.workspace = process.cwd();
42
+ if (!opts.profile) opts.profile = process.env.FLUENT_PROFILE || null;
43
+ return opts;
44
+ }
45
+
46
+ function printHelp() {
47
+ console.log(`
48
+ Workflow Explainer — Fluent Commerce Workflow Analysis & Visualization
49
+
50
+ Usage:
51
+ node tools/workflow-explainer.mjs --workflow <path> [options]
52
+
53
+ Options:
54
+ --workflow, -w <path> Path to workflow JSON file (required)
55
+ --workspace <path> Workspace root (default: cwd)
56
+ --profile, -p <NAME> Account profile name (default: FLUENT_PROFILE env)
57
+ --output, -o <path> Output file path (default: stdout)
58
+ --html Generate self-contained HTML with rendered Mermaid diagrams
59
+ --help, -h Show this help
60
+
61
+ Examples:
62
+ node tools/workflow-explainer.mjs -w ORDER-MULTI.json -p SAGIRISH
63
+ node tools/workflow-explainer.mjs -w ORDER-MULTI.json -p SAGIRISH --html -o report.html
64
+ `);
65
+ }
66
+
67
+ // ─── Workflow Parser ─────────────────────────────────────────────────────────
68
+
69
+ function parseWorkflow(filePath) {
70
+ const raw = readFileSync(filePath, 'utf-8');
71
+ const wf = JSON.parse(raw);
72
+
73
+ const entityTypes = new Set();
74
+ const statusesByEntity = {}; // entityType -> [{name, category}]
75
+ const rulesetsByEntity = {}; // entityType -> [ruleset]
76
+ const allRules = new Map(); // "MODULE.RuleName" -> {name, propsVariants: [{rulesetName, props}]}
77
+ const userActions = []; // [{rulesetName, entityType, subtype, actions}]
78
+ const eventChains = []; // [{from, to, trigger}]
79
+
80
+ // Parse statuses
81
+ for (const s of (wf.statuses || [])) {
82
+ const et = s.entityType || wf.entityType;
83
+ entityTypes.add(et);
84
+ if (!statusesByEntity[et]) statusesByEntity[et] = [];
85
+ if (!statusesByEntity[et].find(x => x.name === s.name)) {
86
+ statusesByEntity[et].push({ name: s.name, category: s.category || '' });
87
+ }
88
+ }
89
+
90
+ // Parse rulesets
91
+ for (const rs of (wf.rulesets || [])) {
92
+ const et = rs.type || wf.entityType;
93
+ entityTypes.add(et);
94
+ if (!rulesetsByEntity[et]) rulesetsByEntity[et] = [];
95
+ rulesetsByEntity[et].push(rs);
96
+
97
+ // Collect rules
98
+ for (const rule of (rs.rules || [])) {
99
+ if (!allRules.has(rule.name)) {
100
+ allRules.set(rule.name, { name: rule.name, propsVariants: [] });
101
+ }
102
+ allRules.get(rule.name).propsVariants.push({
103
+ rulesetName: rs.name,
104
+ entityType: et,
105
+ props: rule.props || null,
106
+ });
107
+ }
108
+
109
+ // Collect user actions
110
+ if (rs.userActions && rs.userActions.length > 0) {
111
+ userActions.push({
112
+ rulesetName: rs.name,
113
+ entityType: et,
114
+ subtype: rs.subtype || null,
115
+ actions: rs.userActions,
116
+ });
117
+ }
118
+
119
+ // Build event chain edges from rules that have eventName/noMatchEventName props
120
+ for (const rule of (rs.rules || [])) {
121
+ if (rule.props) {
122
+ if (rule.props.eventName) {
123
+ eventChains.push({ from: rs.name, to: rule.props.eventName, rule: rule.name, entityType: et });
124
+ }
125
+ if (rule.props.noMatchEventName) {
126
+ eventChains.push({ from: rs.name, to: rule.props.noMatchEventName, rule: rule.name, entityType: et, isNoMatch: true });
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ return {
133
+ name: wf.name,
134
+ version: wf.version,
135
+ entityType: wf.entityType,
136
+ entitySubtype: wf.entitySubtype,
137
+ description: wf.description,
138
+ versionComment: wf.versionComment,
139
+ retailerId: wf.retailerId,
140
+ entityTypes: [...entityTypes],
141
+ statusesByEntity,
142
+ rulesetsByEntity,
143
+ allRules,
144
+ userActions,
145
+ eventChains,
146
+ rulesetCount: (wf.rulesets || []).length,
147
+ statusCount: (wf.statuses || []).length,
148
+ };
149
+ }
150
+
151
+ // ─── Custom Rule Source Finder ───────────────────────────────────────────────
152
+
153
+ function findJavaFiles(dir, results = []) {
154
+ if (!existsSync(dir)) return results;
155
+ try {
156
+ for (const entry of readdirSync(dir)) {
157
+ const full = join(dir, entry);
158
+ try {
159
+ const st = statSync(full);
160
+ if (st.isDirectory()) findJavaFiles(full, results);
161
+ else if (entry.endsWith('.java') && !entry.endsWith('Test.java')) results.push(full);
162
+ } catch { /* skip unreadable */ }
163
+ }
164
+ } catch { /* skip unreadable */ }
165
+ return results;
166
+ }
167
+
168
+ function extractClassName(filePath) {
169
+ return basename(filePath, '.java');
170
+ }
171
+
172
+ function parseJavaSource(filePath) {
173
+ const src = readFileSync(filePath, 'utf-8');
174
+ const className = extractClassName(filePath);
175
+
176
+ // Extract @RuleInfo
177
+ const ruleInfoMatch = src.match(/@RuleInfo\s*\(\s*([\s\S]*?)\)/);
178
+ let ruleInfoName = null;
179
+ let ruleInfoDesc = null;
180
+ if (ruleInfoMatch) {
181
+ const nameM = ruleInfoMatch[1].match(/name\s*=\s*"([^"]+)"/);
182
+ const descM = ruleInfoMatch[1].match(/description\s*=\s*"([^"]+)"/);
183
+ if (nameM) ruleInfoName = nameM[1];
184
+ if (descM) ruleInfoDesc = descM[1];
185
+ }
186
+
187
+ // Extract @ParamString annotations (multiple)
188
+ const paramStrings = [];
189
+ const paramRegex = /@ParamString\s*\(\s*([\s\S]*?)\)/g;
190
+ let pm;
191
+ while ((pm = paramRegex.exec(src)) !== null) {
192
+ const nameM = pm[1].match(/name\s*=\s*"([^"]+)"/);
193
+ const descM = pm[1].match(/description\s*=\s*"([^"]+)"/);
194
+ if (nameM) paramStrings.push({ name: nameM[1], description: descM ? descM[1] : '' });
195
+ }
196
+
197
+ // Extract @EventAttribute annotations
198
+ const eventAttributes = [];
199
+ const eaRegex = /@EventAttribute\s*\(\s*([\s\S]*?)\)/g;
200
+ let ea;
201
+ while ((ea = eaRegex.exec(src)) !== null) {
202
+ const nameM = ea[1].match(/name\s*=\s*"([^"]+)"/);
203
+ const descM = ea[1].match(/description\s*=\s*"([^"]+)"/);
204
+ if (nameM) eventAttributes.push({ name: nameM[1], description: descM ? descM[1] : '' });
205
+ }
206
+
207
+ // Extract class-level Javadoc
208
+ const javadocMatch = src.match(/\/\*\*\s*([\s\S]*?)\s*\*\/\s*(?:@RuleInfo|@ParamString|@Slf4j|public\s+class)/);
209
+ let javadoc = null;
210
+ if (javadocMatch) {
211
+ javadoc = javadocMatch[1]
212
+ .split('\n')
213
+ .map(l => l.replace(/^\s*\*\s?/, '').trim())
214
+ .filter(l => l && !l.startsWith('@'))
215
+ .join(' ')
216
+ .trim();
217
+ }
218
+
219
+ // Extract extends class
220
+ const extendsMatch = src.match(/class\s+\w+\s+extends\s+(\w+)/);
221
+ const extendsClass = extendsMatch ? extendsMatch[1] : null;
222
+
223
+ // Count lines
224
+ const lineCount = src.split('\n').length;
225
+
226
+ // Detect key patterns
227
+ const patterns = [];
228
+ if (src.includes('postWebhook')) patterns.push('WEBHOOK');
229
+ if (src.includes('SendEvent') || src.includes('sendEvent') || src.includes('action().sendEvent')) patterns.push('SENDS_EVENT');
230
+ if (src.includes('SetState') || src.includes('setState') || src.includes('setStatus')) patterns.push('STATUS_CHANGE');
231
+ if (src.includes('DynamicUtils.mutate')) patterns.push('MUTATION');
232
+ if (src.includes('DynamicUtils.query')) patterns.push('QUERY');
233
+ if (src.includes('DynamicUtils.create')) patterns.push('CREATE_ENTITY');
234
+ if (src.includes('SettingUtils.getSetting')) patterns.push('READS_SETTING');
235
+ if (src.includes('getJsonPath')) patterns.push('JSON_PATH');
236
+ if (src.includes('queryList')) patterns.push('LIST_QUERY');
237
+
238
+ return {
239
+ className,
240
+ filePath,
241
+ ruleInfoName,
242
+ ruleInfoDesc,
243
+ paramStrings,
244
+ eventAttributes,
245
+ javadoc,
246
+ extendsClass,
247
+ lineCount,
248
+ patterns,
249
+ };
250
+ }
251
+
252
+ function buildRuleSourceIndex(workspacePath, profile) {
253
+ const index = new Map(); // className -> { source: ParsedJava, isCustom: boolean, modulePath: string }
254
+
255
+ if (!profile || !workspacePath) return index;
256
+
257
+ const backendDir = join(workspacePath, 'accounts', profile, 'SOURCE', 'backend');
258
+ if (!existsSync(backendDir)) return index;
259
+
260
+ // Scan custom source (exclude .decompiled)
261
+ for (const entry of readdirSync(backendDir)) {
262
+ if (entry === '.decompiled') continue;
263
+ const moduleDir = join(backendDir, entry);
264
+ if (!statSync(moduleDir).isDirectory()) continue;
265
+ const javaFiles = findJavaFiles(moduleDir);
266
+ for (const f of javaFiles) {
267
+ const parsed = parseJavaSource(f);
268
+ index.set(parsed.className, {
269
+ source: parsed,
270
+ isCustom: true,
271
+ modulePath: entry,
272
+ });
273
+ }
274
+ }
275
+
276
+ // Scan decompiled OOTB source
277
+ const decompiledDir = join(backendDir, '.decompiled');
278
+ if (existsSync(decompiledDir)) {
279
+ for (const entry of readdirSync(decompiledDir)) {
280
+ const pluginDir = join(decompiledDir, entry);
281
+ if (!statSync(pluginDir).isDirectory()) continue;
282
+ const javaFiles = findJavaFiles(pluginDir);
283
+ for (const f of javaFiles) {
284
+ const parsed = parseJavaSource(f);
285
+ // Don't overwrite custom source with decompiled
286
+ if (!index.has(parsed.className)) {
287
+ index.set(parsed.className, {
288
+ source: parsed,
289
+ isCustom: false,
290
+ modulePath: `.decompiled/${entry}`,
291
+ });
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ return index;
298
+ }
299
+
300
+ function resolveRuleSource(ruleName, sourceIndex) {
301
+ // Rule name format: "NAMESPACE.module.ClassName" or "NAMESPACE.ClassName"
302
+ const parts = ruleName.split('.');
303
+ const className = parts[parts.length - 1];
304
+ return sourceIndex.get(className) || null;
305
+ }
306
+
307
+ // ─── Status Flow Graph Builder ───────────────────────────────────────────────
308
+
309
+ function buildStatusFlows(wfData) {
310
+ // For each entity type, trace status transitions from rulesets
311
+ const flows = {}; // entityType -> [{from, to, via, rules}]
312
+
313
+ for (const [entityType, rulesets] of Object.entries(wfData.rulesetsByEntity)) {
314
+ flows[entityType] = [];
315
+ const edges = new Map(); // "from->to" -> {from, to, via: [rulesetName], rules: [ruleName]}
316
+
317
+ for (const rs of rulesets) {
318
+ const triggerStatuses = (rs.triggers || []).map(t => t.status);
319
+
320
+ // Find status changes in rules (SetState, UpdateStatusHistory)
321
+ for (const rule of (rs.rules || [])) {
322
+ let toStatus = null;
323
+ if (rule.props) {
324
+ if (rule.props.status && (rule.name.includes('SetState') || rule.name.includes('setState'))) {
325
+ toStatus = rule.props.status;
326
+ }
327
+ if (rule.props.toStatus && rule.name.includes('UpdateStatusHistory')) {
328
+ toStatus = rule.props.toStatus;
329
+ }
330
+ }
331
+
332
+ if (toStatus) {
333
+ for (const fromStatus of triggerStatuses) {
334
+ const key = `${fromStatus}->${toStatus}`;
335
+ if (!edges.has(key)) {
336
+ edges.set(key, { from: fromStatus, to: toStatus, via: [], rules: [] });
337
+ }
338
+ const edge = edges.get(key);
339
+ if (!edge.via.includes(rs.name)) edge.via.push(rs.name);
340
+ if (!edge.rules.includes(rule.name)) edge.rules.push(rule.name);
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ flows[entityType] = [...edges.values()];
347
+ }
348
+
349
+ return flows;
350
+ }
351
+
352
+ // ─── Mermaid Diagram Generator ───────────────────────────────────────────────
353
+
354
+ function generateStatusFlowMermaid(entityType, statusFlow, statuses) {
355
+ const lines = ['stateDiagram-v2'];
356
+ lines.push(` direction TB`);
357
+
358
+ // Add state definitions with categories
359
+ const statusNames = new Set();
360
+ for (const edge of statusFlow) {
361
+ statusNames.add(edge.from);
362
+ statusNames.add(edge.to);
363
+ }
364
+
365
+ // Color by category
366
+ const categoryColors = {
367
+ BOOKING: '#4A90D9',
368
+ FULFILMENT: '#E8A838',
369
+ DELIVERY: '#7B68EE',
370
+ DONE: '#5CB85C',
371
+ EXCEPTION: '#D9534F',
372
+ GENERATION: '#9B59B6',
373
+ };
374
+
375
+ for (const s of (statuses || [])) {
376
+ if (statusNames.has(s.name) && s.category && categoryColors[s.category]) {
377
+ lines.push(` state "${s.name}" as ${sanitizeMermaidId(s.name)}`);
378
+ }
379
+ }
380
+
381
+ // Transitions
382
+ for (const edge of statusFlow) {
383
+ const label = edge.via.join(', ');
384
+ const from = sanitizeMermaidId(edge.from);
385
+ const to = sanitizeMermaidId(edge.to);
386
+ lines.push(` ${from} --> ${to} : ${label}`);
387
+ }
388
+
389
+ // Start state
390
+ if (statusFlow.length > 0) {
391
+ const hasCreated = statusNames.has('CREATED');
392
+ if (hasCreated) {
393
+ lines.push(` [*] --> ${sanitizeMermaidId('CREATED')}`);
394
+ }
395
+ }
396
+
397
+ // Terminal states
398
+ const terminalStatuses = ['COMPLETE', 'COMPLETED', 'CANCELLED', 'FAILED'];
399
+ for (const ts of terminalStatuses) {
400
+ if (statusNames.has(ts)) {
401
+ lines.push(` ${sanitizeMermaidId(ts)} --> [*]`);
402
+ }
403
+ }
404
+
405
+ return lines.join('\n');
406
+ }
407
+
408
+ function sanitizeMermaidId(name) {
409
+ return name.replace(/[^a-zA-Z0-9_]/g, '_');
410
+ }
411
+
412
+ function generateEventChainMermaid(wfData) {
413
+ const lines = ['flowchart TD'];
414
+
415
+ // Group by entity type for subgraph
416
+ const byEntity = {};
417
+ for (const chain of wfData.eventChains) {
418
+ const et = chain.entityType;
419
+ if (!byEntity[et]) byEntity[et] = [];
420
+ byEntity[et].push(chain);
421
+ }
422
+
423
+ for (const [et, chains] of Object.entries(byEntity)) {
424
+ lines.push(` subgraph ${et}`);
425
+ for (const c of chains) {
426
+ const from = sanitizeMermaidId(`${et}_${c.from}`);
427
+ const to = sanitizeMermaidId(`${et}_${c.to}`);
428
+ const style = c.isNoMatch ? '-.->' : '-->';
429
+ const label = c.isNoMatch ? 'no-match' : '';
430
+ lines.push(` ${from}["${c.from}"] ${style}|${label}| ${to}["${c.to}"]`);
431
+ }
432
+ lines.push(` end`);
433
+ }
434
+
435
+ // Cross-entity event chains (e.g., ORDER -> FULFILMENT_CHOICE)
436
+ // Detect rules that send events to other entity types
437
+ for (const [et, rulesets] of Object.entries(wfData.rulesetsByEntity)) {
438
+ for (const rs of rulesets) {
439
+ for (const rule of (rs.rules || [])) {
440
+ if (rule.name.includes('SendEventForAllFulfilment') && rule.props?.eventName) {
441
+ const targetET = rule.name.includes('Choice') ? 'FULFILMENT_CHOICE' : 'FULFILMENT';
442
+ if (wfData.rulesetsByEntity[targetET]) {
443
+ const from = sanitizeMermaidId(`${et}_${rs.name}`);
444
+ const to = sanitizeMermaidId(`${targetET}_${rule.props.eventName}`);
445
+ lines.push(` ${from} -.->|"fan-out"| ${to}`);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ return lines.join('\n');
453
+ }
454
+
455
+ // ─── Report Generator ────────────────────────────────────────────────────────
456
+
457
+ function generateMarkdownReport(wfData, sourceIndex, statusFlows, opts) {
458
+ const lines = [];
459
+ const hr = '---';
460
+
461
+ // Header
462
+ lines.push(`# Workflow Explainer: ${wfData.name}`);
463
+ lines.push('');
464
+ lines.push(`> Auto-generated by \`workflow-explainer.mjs\` on ${new Date().toISOString().split('T')[0]}`);
465
+ lines.push('');
466
+
467
+ // Summary table
468
+ lines.push('## Summary');
469
+ lines.push('');
470
+ lines.push('| Property | Value |');
471
+ lines.push('|----------|-------|');
472
+ lines.push(`| **Workflow** | \`${wfData.name}\` |`);
473
+ lines.push(`| **Version** | ${wfData.version} |`);
474
+ lines.push(`| **Root Entity** | ${wfData.entityType} |`);
475
+ lines.push(`| **Subtype** | ${wfData.entitySubtype} |`);
476
+ lines.push(`| **Retailer ID** | ${wfData.retailerId} |`);
477
+ lines.push(`| **Entity Types** | ${wfData.entityTypes.join(', ')} |`);
478
+ lines.push(`| **Total Rulesets** | ${wfData.rulesetCount} |`);
479
+ lines.push(`| **Total Statuses** | ${wfData.statusCount} |`);
480
+ lines.push(`| **Unique Rules** | ${wfData.allRules.size} |`);
481
+ lines.push(`| **User Actions** | ${wfData.userActions.reduce((n, ua) => n + ua.actions.length, 0)} |`);
482
+ lines.push('');
483
+ if (wfData.description) {
484
+ lines.push(`**Description:** ${wfData.description}`);
485
+ lines.push('');
486
+ }
487
+ if (wfData.versionComment) {
488
+ lines.push(`**Version Comment:** ${wfData.versionComment}`);
489
+ lines.push('');
490
+ }
491
+
492
+ // Rule classification
493
+ let customCount = 0;
494
+ let ootbCount = 0;
495
+ let unknownCount = 0;
496
+ for (const [ruleName] of wfData.allRules) {
497
+ const resolved = resolveRuleSource(ruleName, sourceIndex);
498
+ if (resolved) {
499
+ if (resolved.isCustom) customCount++;
500
+ else ootbCount++;
501
+ } else {
502
+ unknownCount++;
503
+ }
504
+ }
505
+ lines.push('### Rule Classification');
506
+ lines.push('');
507
+ lines.push(`| Type | Count |`);
508
+ lines.push(`|------|-------|`);
509
+ lines.push(`| Custom (source found) | ${customCount} |`);
510
+ lines.push(`| OOTB (decompiled) | ${ootbCount} |`);
511
+ lines.push(`| Unresolved (no source) | ${unknownCount} |`);
512
+ lines.push('');
513
+
514
+ lines.push(hr);
515
+ lines.push('');
516
+
517
+ // ─── Per-Entity Sections ───────────────────────────────────────────
518
+ for (const entityType of wfData.entityTypes) {
519
+ lines.push(`## Entity: ${entityType}`);
520
+ lines.push('');
521
+
522
+ // Statuses
523
+ const statuses = wfData.statusesByEntity[entityType] || [];
524
+ if (statuses.length > 0) {
525
+ lines.push('### Statuses');
526
+ lines.push('');
527
+ lines.push('| Status | Category |');
528
+ lines.push('|--------|----------|');
529
+ for (const s of statuses) {
530
+ lines.push(`| ${s.name} | ${s.category} |`);
531
+ }
532
+ lines.push('');
533
+ }
534
+
535
+ // Status Flow Diagram
536
+ const flow = statusFlows[entityType] || [];
537
+ if (flow.length > 0) {
538
+ lines.push('### Status Flow');
539
+ lines.push('');
540
+ lines.push('```mermaid');
541
+ lines.push(generateStatusFlowMermaid(entityType, flow, statuses));
542
+ lines.push('```');
543
+ lines.push('');
544
+
545
+ // Also show transitions as a table
546
+ lines.push('#### Transitions');
547
+ lines.push('');
548
+ lines.push('| From | To | Via Ruleset(s) |');
549
+ lines.push('|------|----|----------------|');
550
+ for (const edge of flow) {
551
+ lines.push(`| ${edge.from} | ${edge.to} | ${edge.via.join(', ')} |`);
552
+ }
553
+ lines.push('');
554
+ }
555
+
556
+ // Rulesets
557
+ const rulesets = wfData.rulesetsByEntity[entityType] || [];
558
+ if (rulesets.length > 0) {
559
+ lines.push(`### Rulesets (${rulesets.length})`);
560
+ lines.push('');
561
+
562
+ for (const rs of rulesets) {
563
+ lines.push(`#### \`${rs.name}\``);
564
+ lines.push('');
565
+ if (rs.description) {
566
+ lines.push(`> ${rs.description}`);
567
+ lines.push('');
568
+ }
569
+
570
+ // Triggers
571
+ const triggers = (rs.triggers || []).map(t => t.status).join(', ');
572
+ lines.push(`**Triggers:** ${triggers || 'none'}`);
573
+ lines.push('');
574
+
575
+ // Rules
576
+ if (rs.rules && rs.rules.length > 0) {
577
+ lines.push('**Rules (execution order):**');
578
+ lines.push('');
579
+ for (let i = 0; i < rs.rules.length; i++) {
580
+ const rule = rs.rules[i];
581
+ const resolved = resolveRuleSource(rule.name, sourceIndex);
582
+ const badge = resolved ? (resolved.isCustom ? ' `CUSTOM`' : ' `OOTB`') : '';
583
+ lines.push(`${i + 1}. **\`${rule.name}\`**${badge}`);
584
+
585
+ // Show props
586
+ if (rule.props && Object.keys(rule.props).length > 0) {
587
+ for (const [k, v] of Object.entries(rule.props)) {
588
+ const val = typeof v === 'object' ? JSON.stringify(v) : v;
589
+ lines.push(` - \`${k}\`: \`${val}\``);
590
+ }
591
+ }
592
+
593
+ // Show source summary if available
594
+ if (resolved && resolved.source) {
595
+ const src = resolved.source;
596
+ if (src.ruleInfoDesc) {
597
+ lines.push(` - *${src.ruleInfoDesc}*`);
598
+ }
599
+ if (src.patterns.length > 0) {
600
+ lines.push(` - Patterns: ${src.patterns.join(', ')}`);
601
+ }
602
+ }
603
+ }
604
+ lines.push('');
605
+ }
606
+
607
+ // User Actions
608
+ if (rs.userActions && rs.userActions.length > 0) {
609
+ lines.push('**User Actions:**');
610
+ lines.push('');
611
+ for (const ua of rs.userActions) {
612
+ const label = ua.label || ua.name || '(unnamed)';
613
+ lines.push(`- **${label}** → event: \`${ua.eventName || '(none)'}\``);
614
+ if (ua.attributes && ua.attributes.length > 0) {
615
+ for (const attr of ua.attributes) {
616
+ lines.push(` - \`${attr.name}\` (${attr.type}): ${attr.label || ''}`);
617
+ }
618
+ }
619
+ }
620
+ lines.push('');
621
+ }
622
+ }
623
+ }
624
+
625
+ lines.push(hr);
626
+ lines.push('');
627
+ }
628
+
629
+ // ─── Event Chain Diagram ───────────────────────────────────────────
630
+ if (wfData.eventChains.length > 0) {
631
+ lines.push('## Event Chain Map');
632
+ lines.push('');
633
+ lines.push('```mermaid');
634
+ lines.push(generateEventChainMermaid(wfData));
635
+ lines.push('```');
636
+ lines.push('');
637
+
638
+ lines.push('### Event Chain Details');
639
+ lines.push('');
640
+ lines.push('| Source Ruleset | Target Ruleset | Rule | Entity | Type |');
641
+ lines.push('|----------------|----------------|------|--------|------|');
642
+ for (const c of wfData.eventChains) {
643
+ const type = c.isNoMatch ? 'no-match' : 'match';
644
+ lines.push(`| ${c.from} | ${c.to} | ${c.rule.split('.').pop()} | ${c.entityType} | ${type} |`);
645
+ }
646
+ lines.push('');
647
+ lines.push(hr);
648
+ lines.push('');
649
+ }
650
+
651
+ // ─── Custom Rules Inventory ────────────────────────────────────────
652
+ lines.push('## Custom Rules Inventory');
653
+ lines.push('');
654
+
655
+ const customRules = [];
656
+ const ootbRules = [];
657
+ const unresolvedRules = [];
658
+
659
+ for (const [ruleName, ruleData] of wfData.allRules) {
660
+ const resolved = resolveRuleSource(ruleName, sourceIndex);
661
+ if (resolved) {
662
+ if (resolved.isCustom) customRules.push({ ruleName, ruleData, resolved });
663
+ else ootbRules.push({ ruleName, ruleData, resolved });
664
+ } else {
665
+ unresolvedRules.push({ ruleName, ruleData });
666
+ }
667
+ }
668
+
669
+ if (customRules.length > 0) {
670
+ lines.push(`### Custom Rules (${customRules.length})`);
671
+ lines.push('');
672
+
673
+ for (const { ruleName, ruleData, resolved } of customRules) {
674
+ const src = resolved.source;
675
+ lines.push(`#### \`${ruleName}\``);
676
+ lines.push('');
677
+ lines.push(`| Property | Value |`);
678
+ lines.push(`|----------|-------|`);
679
+ lines.push(`| **Module** | \`${resolved.modulePath}\` |`);
680
+ lines.push(`| **Class** | \`${src.className}\` |`);
681
+ lines.push(`| **Extends** | \`${src.extendsClass || 'N/A'}\` |`);
682
+ lines.push(`| **Lines** | ${src.lineCount} |`);
683
+ if (src.ruleInfoDesc) lines.push(`| **Description** | ${src.ruleInfoDesc} |`);
684
+ lines.push(`| **Used in** | ${ruleData.propsVariants.map(p => p.rulesetName).join(', ')} |`);
685
+ lines.push('');
686
+
687
+ // Javadoc summary
688
+ if (src.javadoc) {
689
+ const truncated = src.javadoc.length > 300 ? src.javadoc.slice(0, 300) + '...' : src.javadoc;
690
+ lines.push(`**Purpose:** ${truncated}`);
691
+ lines.push('');
692
+ }
693
+
694
+ // Params
695
+ if (src.paramStrings.length > 0) {
696
+ lines.push('**@ParamString props:**');
697
+ lines.push('');
698
+ lines.push('| Prop | Description |');
699
+ lines.push('|------|-------------|');
700
+ for (const p of src.paramStrings) {
701
+ lines.push(`| \`${p.name}\` | ${p.description} |`);
702
+ }
703
+ lines.push('');
704
+ }
705
+
706
+ // Event Attributes
707
+ if (src.eventAttributes.length > 0) {
708
+ lines.push('**@EventAttribute inputs:**');
709
+ lines.push('');
710
+ lines.push('| Attribute | Description |');
711
+ lines.push('|-----------|-------------|');
712
+ for (const a of src.eventAttributes) {
713
+ lines.push(`| \`${a.name}\` | ${a.description} |`);
714
+ }
715
+ lines.push('');
716
+ }
717
+
718
+ // Behavioral patterns
719
+ if (src.patterns.length > 0) {
720
+ lines.push(`**Behavioral patterns:** ${src.patterns.join(', ')}`);
721
+ lines.push('');
722
+ }
723
+
724
+ // Props variants across rulesets
725
+ if (ruleData.propsVariants.length > 1) {
726
+ lines.push('**Configuration variants:**');
727
+ lines.push('');
728
+ lines.push('| Ruleset | Entity | Key Props |');
729
+ lines.push('|---------|--------|-----------|');
730
+ for (const v of ruleData.propsVariants) {
731
+ const keyProps = v.props ? Object.entries(v.props).map(([k, val]) => `${k}=${typeof val === 'object' ? JSON.stringify(val) : val}`).join(', ') : '(none)';
732
+ lines.push(`| ${v.rulesetName} | ${v.entityType} | ${keyProps} |`);
733
+ }
734
+ lines.push('');
735
+ }
736
+ }
737
+ }
738
+
739
+ if (ootbRules.length > 0) {
740
+ lines.push(`### OOTB Rules (${ootbRules.length})`);
741
+ lines.push('');
742
+ lines.push('| Rule | Class | Used In |');
743
+ lines.push('|------|-------|---------|');
744
+ for (const { ruleName, ruleData, resolved } of ootbRules) {
745
+ const usedIn = ruleData.propsVariants.map(p => p.rulesetName).join(', ');
746
+ lines.push(`| \`${ruleName}\` | ${resolved.source.className} | ${usedIn} |`);
747
+ }
748
+ lines.push('');
749
+ }
750
+
751
+ if (unresolvedRules.length > 0) {
752
+ lines.push(`### Unresolved Rules (${unresolvedRules.length})`);
753
+ lines.push('');
754
+ lines.push('| Rule | Used In | Inferred Behavior |');
755
+ lines.push('|------|---------|-------------------|');
756
+ for (const { ruleName, ruleData } of unresolvedRules) {
757
+ const usedIn = ruleData.propsVariants.map(p => p.rulesetName).join(', ');
758
+ const inferred = inferBehavior(ruleName, ruleData);
759
+ lines.push(`| \`${ruleName}\` | ${usedIn} | ${inferred} |`);
760
+ }
761
+ lines.push('');
762
+ }
763
+
764
+ lines.push(hr);
765
+ lines.push('');
766
+
767
+ // ─── User Actions Summary ─────────────────────────────────────────
768
+ if (wfData.userActions.length > 0) {
769
+ lines.push('## User Actions');
770
+ lines.push('');
771
+ lines.push('| Ruleset | Entity | Subtype | Action | Event |');
772
+ lines.push('|---------|--------|---------|--------|-------|');
773
+ for (const ua of wfData.userActions) {
774
+ for (const action of ua.actions) {
775
+ lines.push(`| ${ua.rulesetName} | ${ua.entityType} | ${ua.subtype || '-'} | ${action.label || action.name || '-'} | \`${action.eventName || '-'}\` |`);
776
+ }
777
+ }
778
+ lines.push('');
779
+ lines.push(hr);
780
+ lines.push('');
781
+ }
782
+
783
+ // ─── Integration Points ────────────────────────────────────────────
784
+ lines.push('## Integration Points');
785
+ lines.push('');
786
+
787
+ // Webhooks
788
+ const webhooks = [];
789
+ for (const [ruleName, ruleData] of wfData.allRules) {
790
+ if (ruleName.toLowerCase().includes('webhook') || ruleName.toLowerCase().includes('Webhook')) {
791
+ for (const v of ruleData.propsVariants) {
792
+ if (v.props?.setting) {
793
+ webhooks.push({ ruleset: v.rulesetName, setting: v.props.setting, entity: v.entityType });
794
+ }
795
+ }
796
+ }
797
+ }
798
+ if (webhooks.length > 0) {
799
+ lines.push('### Webhooks');
800
+ lines.push('');
801
+ lines.push('| Ruleset | Setting | Entity |');
802
+ lines.push('|---------|---------|--------|');
803
+ for (const wh of webhooks) {
804
+ lines.push(`| ${wh.ruleset} | \`${wh.setting}\` | ${wh.entity} |`);
805
+ }
806
+ lines.push('');
807
+ }
808
+
809
+ // Scheduled/timeout events
810
+ const timeouts = [];
811
+ for (const [, rulesets] of Object.entries(wfData.rulesetsByEntity)) {
812
+ for (const rs of rulesets) {
813
+ if (rs.name.toLowerCase().includes('timeout') || rs.name.toLowerCase().includes('expired') ||
814
+ rs.name.toLowerCase().includes('schedule') || rs.name.toLowerCase().includes('grace')) {
815
+ timeouts.push({ name: rs.name, description: rs.description || '' });
816
+ }
817
+ }
818
+ }
819
+ if (timeouts.length > 0) {
820
+ lines.push('### Scheduled/Timeout Events');
821
+ lines.push('');
822
+ for (const t of timeouts) {
823
+ lines.push(`- **${t.name}**: ${t.description.slice(0, 120)}${t.description.length > 120 ? '...' : ''}`);
824
+ }
825
+ lines.push('');
826
+ }
827
+
828
+ // External inbound events (events that are triggered externally)
829
+ const inboundPatterns = ['Confirm', 'Reject', 'Notify', 'Cancel', 'Approve', 'Complete'];
830
+ const inboundEvents = [];
831
+ for (const [et, rulesets] of Object.entries(wfData.rulesetsByEntity)) {
832
+ for (const rs of rulesets) {
833
+ if (inboundPatterns.some(p => rs.name.startsWith(p)) && rs.description?.toLowerCase().includes('inbound')) {
834
+ inboundEvents.push({ name: rs.name, entity: et, description: rs.description || '' });
835
+ }
836
+ }
837
+ }
838
+ if (inboundEvents.length > 0) {
839
+ lines.push('### Inbound Events (External Integration)');
840
+ lines.push('');
841
+ lines.push('| Event | Entity | Purpose |');
842
+ lines.push('|-------|--------|---------|');
843
+ for (const ie of inboundEvents) {
844
+ const purpose = ie.description.slice(0, 100) + (ie.description.length > 100 ? '...' : '');
845
+ lines.push(`| \`${ie.name}\` | ${ie.entity} | ${purpose} |`);
846
+ }
847
+ lines.push('');
848
+ }
849
+
850
+ lines.push(hr);
851
+ lines.push('');
852
+ lines.push(`*Generated from \`${basename(opts.workflow)}\` v${wfData.version}*`);
853
+
854
+ return lines.join('\n');
855
+ }
856
+
857
+ function inferBehavior(ruleName, ruleData) {
858
+ const name = ruleName.toLowerCase();
859
+ const parts = ruleName.split('.');
860
+ const className = parts[parts.length - 1];
861
+
862
+ if (name.includes('setstate') || name.includes('setState')) return 'Sets entity status';
863
+ if (name.includes('sendevent')) {
864
+ const events = ruleData.propsVariants.filter(p => p.props?.eventName).map(p => p.props.eventName);
865
+ return events.length > 0 ? `Sends event: ${events.join(', ')}` : 'Sends inline event';
866
+ }
867
+ if (name.includes('webhook')) return 'Posts webhook to external system';
868
+ if (name.includes('upsert') && name.includes('attribute')) return 'Upserts entity attribute';
869
+ if (name.includes('create') && name.includes('fulfilment')) return 'Creates fulfilment entity';
870
+ if (name.includes('updatestatus')) return 'Updates status history attribute';
871
+ if (name.includes('verify') || name.includes('check')) return 'Conditional gate/check';
872
+ if (name.includes('cancel')) return 'Cancellation logic';
873
+ return `Inferred from name: ${className}`;
874
+ }
875
+
876
+ // ─── HTML Report Generator ───────────────────────────────────────────────────
877
+
878
+ function wrapInHtml(markdownContent, title) {
879
+ return `<!DOCTYPE html>
880
+ <html lang="en">
881
+ <head>
882
+ <meta charset="UTF-8">
883
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
884
+ <title>${escapeHtml(title)}</title>
885
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
886
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"><\/script>
887
+ <style>
888
+ :root {
889
+ --fc-primary: #1a1a2e;
890
+ --fc-accent: #e94560;
891
+ --fc-bg: #f8f9fa;
892
+ --fc-card: #ffffff;
893
+ --fc-border: #e0e0e0;
894
+ --fc-text: #2d2d2d;
895
+ --fc-muted: #6c757d;
896
+ }
897
+ * { box-sizing: border-box; margin: 0; padding: 0; }
898
+ body {
899
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
900
+ background: var(--fc-bg);
901
+ color: var(--fc-text);
902
+ line-height: 1.6;
903
+ padding: 2rem;
904
+ max-width: 1200px;
905
+ margin: 0 auto;
906
+ }
907
+ h1 { color: var(--fc-primary); border-bottom: 3px solid var(--fc-accent); padding-bottom: 0.5rem; margin-bottom: 1.5rem; }
908
+ h2 { color: var(--fc-primary); margin-top: 2rem; margin-bottom: 1rem; border-bottom: 1px solid var(--fc-border); padding-bottom: 0.3rem; }
909
+ h3 { color: var(--fc-primary); margin-top: 1.5rem; margin-bottom: 0.5rem; }
910
+ h4 { margin-top: 1.2rem; margin-bottom: 0.4rem; color: var(--fc-accent); }
911
+ table { border-collapse: collapse; width: 100%; margin: 0.5rem 0 1rem; }
912
+ th, td { border: 1px solid var(--fc-border); padding: 0.5rem 0.75rem; text-align: left; }
913
+ th { background: var(--fc-primary); color: white; font-weight: 600; }
914
+ tr:nth-child(even) { background: #f1f3f5; }
915
+ code { background: #e9ecef; padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.9em; }
916
+ pre { background: #2d2d2d; color: #f8f8f2; padding: 1rem; border-radius: 6px; overflow-x: auto; margin: 1rem 0; }
917
+ pre code { background: none; color: inherit; padding: 0; }
918
+ blockquote { border-left: 4px solid var(--fc-accent); padding: 0.5rem 1rem; margin: 0.5rem 0; background: #fff3f5; }
919
+ hr { border: none; border-top: 1px solid var(--fc-border); margin: 1.5rem 0; }
920
+ ul, ol { padding-left: 1.5rem; margin: 0.5rem 0; }
921
+ li { margin: 0.2rem 0; }
922
+ .mermaid { background: white; padding: 1rem; border-radius: 6px; border: 1px solid var(--fc-border); margin: 1rem 0; text-align: center; }
923
+ em { color: var(--fc-muted); }
924
+ strong { color: var(--fc-primary); }
925
+ a { color: var(--fc-accent); text-decoration: none; }
926
+ a:hover { text-decoration: underline; }
927
+ </style>
928
+ </head>
929
+ <body>
930
+ <div id="content"></div>
931
+ <script>
932
+ mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose' });
933
+ const md = ${JSON.stringify(markdownContent)};
934
+ const renderer = new marked.Renderer();
935
+ renderer.code = function(codeObj) {
936
+ const text = typeof codeObj === 'object' ? codeObj.text : codeObj;
937
+ const lang = typeof codeObj === 'object' ? codeObj.lang : arguments[1];
938
+ if (lang === 'mermaid') {
939
+ return '<div class="mermaid">' + text + '</div>';
940
+ }
941
+ return '<pre><code>' + (typeof text === 'string' ? text.replace(/</g, '&lt;').replace(/>/g, '&gt;') : text) + '</code></pre>';
942
+ };
943
+ document.getElementById('content').innerHTML = marked.parse(md, { renderer });
944
+ mermaid.run();
945
+ </script>
946
+ </body>
947
+ </html>`;
948
+ }
949
+
950
+ function escapeHtml(str) {
951
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
952
+ }
953
+
954
+ // ─── Main ────────────────────────────────────────────────────────────────────
955
+
956
+ function main() {
957
+ const opts = parseArgs();
958
+
959
+ // Resolve workflow path
960
+ let workflowPath = opts.workflow;
961
+ if (!existsSync(workflowPath)) {
962
+ workflowPath = resolve(opts.workspace, workflowPath);
963
+ }
964
+ if (!existsSync(workflowPath)) {
965
+ console.error(`Error: Workflow file not found: ${opts.workflow}`);
966
+ console.error(` Tried: ${opts.workflow}`);
967
+ console.error(` Tried: ${workflowPath}`);
968
+ process.exit(1);
969
+ }
970
+
971
+ console.error(`[workflow-explainer] Parsing: ${workflowPath}`);
972
+
973
+ // Parse workflow
974
+ const wfData = parseWorkflow(workflowPath);
975
+ console.error(`[workflow-explainer] Workflow: ${wfData.name} v${wfData.version}`);
976
+ console.error(`[workflow-explainer] Entities: ${wfData.entityTypes.join(', ')}`);
977
+ console.error(`[workflow-explainer] Rulesets: ${wfData.rulesetCount}, Statuses: ${wfData.statusCount}, Rules: ${wfData.allRules.size}`);
978
+
979
+ // Build source index
980
+ console.error(`[workflow-explainer] Scanning source code...`);
981
+ const sourceIndex = buildRuleSourceIndex(opts.workspace, opts.profile);
982
+ console.error(`[workflow-explainer] Source index: ${sourceIndex.size} classes (${[...sourceIndex.values()].filter(v => v.isCustom).length} custom, ${[...sourceIndex.values()].filter(v => !v.isCustom).length} OOTB)`);
983
+
984
+ // Match rules to source
985
+ let matched = 0;
986
+ for (const [ruleName] of wfData.allRules) {
987
+ if (resolveRuleSource(ruleName, sourceIndex)) matched++;
988
+ }
989
+ console.error(`[workflow-explainer] Rule resolution: ${matched}/${wfData.allRules.size} rules matched to source`);
990
+
991
+ // Build status flows
992
+ const statusFlows = buildStatusFlows(wfData);
993
+ for (const [et, flows] of Object.entries(statusFlows)) {
994
+ console.error(`[workflow-explainer] ${et}: ${flows.length} status transitions`);
995
+ }
996
+
997
+ // Generate report
998
+ const markdown = generateMarkdownReport(wfData, sourceIndex, statusFlows, opts);
999
+
1000
+ // Output
1001
+ if (opts.html) {
1002
+ const html = wrapInHtml(markdown, `Workflow: ${wfData.name}`);
1003
+ if (opts.output) {
1004
+ writeFileSync(opts.output, html, 'utf-8');
1005
+ console.error(`[workflow-explainer] HTML report written to: ${opts.output}`);
1006
+ } else {
1007
+ process.stdout.write(html);
1008
+ }
1009
+ } else {
1010
+ if (opts.output) {
1011
+ writeFileSync(opts.output, markdown, 'utf-8');
1012
+ console.error(`[workflow-explainer] Markdown report written to: ${opts.output}`);
1013
+ } else {
1014
+ process.stdout.write(markdown);
1015
+ }
1016
+ }
1017
+
1018
+ console.error(`[workflow-explainer] Done.`);
1019
+ }
1020
+
1021
+ main();