@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.
- package/README.md +17 -12
- package/bin/cli.mjs +219 -43
- package/content/cli/skills/fluent-bootstrap/SKILL.md +1 -1
- package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +1 -1
- package/content/cli/skills/fluent-cli-reference/SKILL.md +1 -1
- package/content/cli/skills/fluent-cli-retailer/SKILL.md +1 -1
- package/content/cli/skills/fluent-connect/SKILL.md +58 -3
- package/content/cli/skills/fluent-profile/SKILL.md +35 -5
- package/content/cli/skills/fluent-workflow/SKILL.md +2 -1
- package/content/dev/agents/fluent-backend-dev.md +2 -2
- package/content/dev/agents/fluent-dev.md +1 -1
- package/content/dev/agents/fluent-frontend-dev.md +1 -1
- package/content/dev/skills/fluent-account-snapshot/SKILL.md +1 -1
- package/content/dev/skills/fluent-archive/SKILL.md +2 -1
- package/content/dev/skills/fluent-build/SKILL.md +2 -2
- package/content/dev/skills/fluent-connection-analysis/SKILL.md +2 -1
- package/content/dev/skills/fluent-custom-code/SKILL.md +3 -2
- package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +7 -6
- package/content/dev/skills/fluent-e2e-test/SKILL.md +1 -1
- package/content/dev/skills/fluent-entity-flow-diagnose/SKILL.md +2 -1
- package/content/dev/skills/fluent-event-api/SKILL.md +3 -2
- package/content/dev/skills/fluent-feature-explain/SKILL.md +2 -1
- package/content/dev/skills/fluent-feature-plan/SKILL.md +2 -1
- package/content/dev/skills/fluent-feature-status/SKILL.md +1 -1
- package/content/dev/skills/fluent-frontend-build/SKILL.md +1 -1
- package/content/dev/skills/fluent-frontend-readme/SKILL.md +2 -1
- package/content/dev/skills/fluent-frontend-review/SKILL.md +2 -1
- package/content/dev/skills/fluent-goal/SKILL.md +1 -1
- package/content/dev/skills/fluent-implementation-map/SKILL.md +1 -1
- package/content/dev/skills/fluent-inventory-catalog/SKILL.md +2 -2
- package/content/dev/skills/fluent-job-batch/SKILL.md +6 -3
- package/content/dev/skills/fluent-knowledge-init/SKILL.md +1 -1
- package/content/dev/skills/{fluent-source-onboard → fluent-module-convert}/SKILL.md +223 -24
- package/content/dev/skills/fluent-module-scaffold/SKILL.md +2 -1
- package/content/dev/skills/fluent-module-validate/SKILL.md +8 -7
- package/content/dev/skills/fluent-mystique-assess/SKILL.md +22 -6
- package/content/dev/skills/fluent-mystique-builder/SKILL.md +33 -2
- package/content/dev/skills/fluent-mystique-component/SKILL.md +1 -1
- package/content/dev/skills/fluent-mystique-diff/SKILL.md +1 -1
- package/content/dev/skills/fluent-mystique-preview/SKILL.md +19 -2
- package/content/dev/skills/fluent-mystique-scaffold/SDK_REFERENCE.md +2 -2
- package/content/dev/skills/fluent-mystique-scaffold/SKILL.md +13 -2
- package/content/dev/skills/fluent-mystique-scaffold/TEMPLATES.md +1 -1
- package/content/dev/skills/fluent-mystique-sdk-reference/SKILL.md +2 -1
- package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +3 -3
- package/content/dev/skills/fluent-retailer-config/SKILL.md +3 -3
- package/content/dev/skills/fluent-rollback/SKILL.md +1 -1
- package/content/dev/skills/fluent-rule-lookup/SKILL.md +2 -1
- package/content/dev/skills/fluent-rule-scaffold/SKILL.md +7 -6
- package/content/dev/skills/fluent-rule-test/SKILL.md +2 -1
- package/content/dev/skills/fluent-scope-plan/SKILL.md +2 -2
- package/content/dev/skills/fluent-session/SKILL.md +1 -1
- package/content/dev/skills/fluent-settings/SKILL.md +2 -2
- package/content/dev/skills/fluent-skill-observability/SKILL.md +1 -1
- package/content/dev/skills/fluent-sourcing/SKILL.md +38 -13
- package/content/dev/skills/fluent-system-monitoring/SKILL.md +1 -1
- package/content/dev/skills/fluent-test-data/SKILL.md +1 -1
- package/content/dev/skills/fluent-trace/SKILL.md +3 -2
- package/content/dev/skills/fluent-transition-api/SKILL.md +3 -2
- package/content/dev/skills/fluent-ui-record/SKILL.md +1 -1
- package/content/dev/skills/fluent-ui-test/SKILL.md +9 -8
- package/content/dev/skills/fluent-use-case-discover/SKILL.md +2 -2
- package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +3 -2
- package/content/dev/skills/fluent-workspace-tree/SKILL.md +4 -3
- package/content/knowledge/index.md +3 -3
- package/content/knowledge/platform/domain-model.md +1 -0
- package/content/knowledge/platform/module-structure.md +5 -5
- package/content/knowledge/platform/mystique-routing.md +6 -3
- package/content/knowledge/platform/permissions-and-contexts.md +2 -2
- package/content/knowledge/platform/rule-test-patterns.md +1 -1
- package/content/knowledge/platform/workflow-json-structure.md +1 -1
- package/content/mcp-extn/skills/fluent-mcp-core/SKILL.md +2 -1
- package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +3 -2
- package/content/rfl/skills/fluent-rfl-assess/SKILL.md +1 -1
- package/docs/01-first-session.md +175 -0
- package/docs/02-prompt-guide.md +246 -0
- package/docs/03-use-cases.md +1181 -0
- package/docs/04-onboarding-plan.md +355 -0
- package/docs/05-getting-started.md +262 -0
- package/docs/06-dev-workflow.md +1040 -0
- package/docs/INDEX.md +40 -0
- package/docs/agents-and-skills-guide.md +199 -0
- package/docs/capability-map.md +165 -0
- package/docs/chrome-devtools-mcp-reference.md +401 -0
- package/docs/fluent-ai-skills-reference.md +1351 -0
- package/docs/manifest-safety.md +79 -0
- package/docs/mcp-servers.md +209 -0
- package/docs/workflow-reference.md +167 -0
- package/lib/fluent-brand.css +55 -0
- package/metadata.json +7 -6
- package/package.json +17 -3
- package/scripts/postinstall.mjs +38 -0
- package/{content/dev/skills/fluent-trace/scripts/analyze-event-capture.mjs → tools/event-capture-analyzer.mjs} +3 -3
- package/tools/{generate-feature-dashboard.mjs → feature-dashboard.mjs} +2 -2
- package/tools/manifest-diff.mjs +1 -1
- package/{content/dev/skills/fluent-mystique-assess/validator.mjs → tools/manifest-validator.mjs} +2 -2
- 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, '<').replace(/>/g, '>') : 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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();
|