@atlashub/smartstack-cli 4.53.0 → 4.55.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/package.json +1 -1
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +64 -1
- package/templates/skills/business-analyse/steps/step-03-specify.md +33 -1
- package/templates/skills/business-analyse-html/SKILL.md +2 -0
- package/templates/skills/business-analyse-html/html/ba-interactive.html +30 -3
- package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +30 -3
- package/templates/skills/business-analyse-html/steps/step-02-build-data.md +82 -35
- package/templates/skills/business-analyse-html/steps/step-03-render.md +17 -0
package/package.json
CHANGED
|
@@ -120,11 +120,68 @@ IF docs/**/*.md files found:
|
|
|
120
120
|
- Pre-identify integration points: new feature may depend on or extend existing modules
|
|
121
121
|
- NEVER re-specify what's already handed-off (status = "handed-off") — reference it instead
|
|
122
122
|
|
|
123
|
+
### 2d. Domain Research (WebSearch — BEFORE engaging the client)
|
|
124
|
+
|
|
125
|
+
> **Ground the analysis in real-world patterns. This prevents greenfield mistakes
|
|
126
|
+
> (e.g., putting firstName on Employee instead of linking to User).**
|
|
127
|
+
> This step takes 30-60 seconds and dramatically improves entity design quality.
|
|
128
|
+
|
|
129
|
+
**WHEN to search:** ALWAYS for the main business domain. Skip only if the domain is purely SmartStack-internal (e.g., config, admin tools).
|
|
130
|
+
|
|
131
|
+
**HOW to search:** Use WebSearch tool with targeted queries based on the detected domain.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
1. DOMAIN MODEL SEARCH
|
|
135
|
+
Query: "{detected_domain} data model best practices" (e.g., "HR management data model best practices")
|
|
136
|
+
→ Extract: standard entities, common relationships, industry patterns
|
|
137
|
+
→ Store key findings in {domain_research}.dataModel
|
|
138
|
+
|
|
139
|
+
2. ENTITY PATTERN SEARCH (for the 2-3 most complex detected entities)
|
|
140
|
+
Query: "{entity_name} entity design pattern ERP" (e.g., "employee entity design pattern ERP")
|
|
141
|
+
→ Extract: standard attributes, versioning patterns, common pitfalls
|
|
142
|
+
→ Store in {domain_research}.entityPatterns[entity_name]
|
|
143
|
+
|
|
144
|
+
3. WORKFLOW SEARCH (if the domain involves approval/validation flows)
|
|
145
|
+
Query: "{domain} approval workflow pattern" (e.g., "timesheet approval workflow pattern")
|
|
146
|
+
→ Extract: standard states, transition rules, multi-level approval
|
|
147
|
+
→ Store in {domain_research}.workflows
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**WHAT to extract (ULTRATHINK — internal only):**
|
|
151
|
+
```yaml
|
|
152
|
+
domain_research:
|
|
153
|
+
domain: "{detected domain — e.g., HR, CRM, Billing}"
|
|
154
|
+
dataModel:
|
|
155
|
+
standardEntities: ["Entity1", "Entity2"] # what the industry considers standard
|
|
156
|
+
commonRelationships: ["Entity1 → Entity2"] # typical FK patterns
|
|
157
|
+
antiPatterns: ["storing salary directly on employee"] # mistakes to avoid
|
|
158
|
+
entityPatterns:
|
|
159
|
+
Employee:
|
|
160
|
+
standardAttributes: ["code", "userId", "departmentId", "hireDate", "status"]
|
|
161
|
+
versionedData: ["salary → separate table with effectiveDate"]
|
|
162
|
+
personPattern: "link to User via FK, never duplicate personal info"
|
|
163
|
+
Invoice:
|
|
164
|
+
standardAttributes: ["number", "clientId", "issueDate", "dueDate", "status"]
|
|
165
|
+
calculatedFields: ["subtotal", "taxAmount", "total"]
|
|
166
|
+
workflowStates: ["Draft", "Sent", "Paid", "Overdue", "Cancelled"]
|
|
167
|
+
workflows:
|
|
168
|
+
timesheetApproval: "Draft → Submitted → ManagerApproved → ClientApproved"
|
|
169
|
+
invoiceLifecycle: "Draft → Validated → Sent → Paid"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> **CRITICAL:** This research is INTERNAL context. Do NOT show raw search results to the client.
|
|
173
|
+
> Use the findings to:
|
|
174
|
+
> - Improve pre-analysis quality (detect shadow zones the client won't mention)
|
|
175
|
+
> - Ground entity architecture decisions in Phase 4d-bis
|
|
176
|
+
> - Inform proactive suggestions in Phase 4 (ANTICIPATION)
|
|
177
|
+
> - Prevent common anti-patterns during step-03 entity specification
|
|
178
|
+
|
|
123
179
|
### 2b. Silent Pre-Analysis (ULTRATHINK — no output to client)
|
|
124
180
|
|
|
125
181
|
> **The AI prepares the conversation before speaking. This is NOT output — it's internal reasoning.**
|
|
182
|
+
> **INCLUDES domain research findings from 2d above.**
|
|
126
183
|
|
|
127
|
-
Analyze `{feature_description}` silently:
|
|
184
|
+
Analyze `{feature_description}` AND `{domain_research}` silently:
|
|
128
185
|
|
|
129
186
|
1. **Identify problem type** from keywords (replace, automate, centralize, new tool)
|
|
130
187
|
2. **Extract explicit modules/features** mentioned in the description
|
|
@@ -143,6 +200,12 @@ _preAnalysis:
|
|
|
143
200
|
detected_modules: [{name, description, detected_sections}]
|
|
144
201
|
shadow_zones: [{topic, why_it_matters, challenge_question}]
|
|
145
202
|
anticipated_suggestions: [{suggestion, justification, module}]
|
|
203
|
+
domain_research: # from Phase 2d web search
|
|
204
|
+
domain: "{detected domain}"
|
|
205
|
+
standardEntities: [...]
|
|
206
|
+
antiPatterns: [...]
|
|
207
|
+
entityPatterns: {entity: {standardAttributes, versionedData, personPattern}}
|
|
208
|
+
workflows: {name: "state1 → state2 → state3"}
|
|
146
209
|
```
|
|
147
210
|
|
|
148
211
|
---
|
|
@@ -43,9 +43,41 @@ For each module, execute these sub-steps:
|
|
|
43
43
|
|
|
44
44
|
Load via ba-reader:
|
|
45
45
|
- Module's `index.json` (if exists from previous run)
|
|
46
|
-
- Application `cadrage.json` for stakeholder context
|
|
46
|
+
- Application `cadrage.json` for stakeholder context (including `_preAnalysis.domain_research`)
|
|
47
47
|
- Completed modules' summaries (compact, <100 lines each)
|
|
48
48
|
|
|
49
|
+
### A-bis. Entity Pattern Research (WebSearch — per module)
|
|
50
|
+
|
|
51
|
+
> **Before defining entities, validate design against industry best practices.**
|
|
52
|
+
> Uses domain_research from step-01 as baseline, then does targeted searches for this specific module.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
IF cadrage._preAnalysis.domain_research exists:
|
|
56
|
+
→ Load as baseline (already researched in step-01)
|
|
57
|
+
ELSE:
|
|
58
|
+
→ Run a quick WebSearch: "{module_domain} entity data model best practices"
|
|
59
|
+
|
|
60
|
+
FOR each main entity in this module (max 2-3 searches):
|
|
61
|
+
Query: "{entity_name} entity design pattern {domain}"
|
|
62
|
+
(e.g., "invoice entity design ERP", "timesheet data model best practices")
|
|
63
|
+
|
|
64
|
+
EXTRACT (ULTRATHINK — internal only):
|
|
65
|
+
- Standard attributes for this entity type
|
|
66
|
+
- Common relationships and cardinality
|
|
67
|
+
- Versioned vs. inline data patterns (e.g., salary history)
|
|
68
|
+
- Workflow states if applicable
|
|
69
|
+
- Known anti-patterns to avoid
|
|
70
|
+
|
|
71
|
+
CROSS-CHECK against SmartStack conventions (B-bis rules):
|
|
72
|
+
- Would this entity trigger Person Extension Pattern?
|
|
73
|
+
- Are there attributes that should be versioned (salary, rate)?
|
|
74
|
+
- Does this entity already exist in the SmartStack socle DB?
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> **CRITICAL:** This research informs entity design but does NOT override SmartStack conventions.
|
|
78
|
+
> If web research suggests putting firstName on Employee, the B-bis guard still blocks it
|
|
79
|
+
> because SmartStack uses auth_Users for personal data.
|
|
80
|
+
|
|
49
81
|
### B. Entities
|
|
50
82
|
|
|
51
83
|
For each entity identified in step 02:
|
|
@@ -49,6 +49,8 @@ Generate the interactive HTML document of the business analysis from the JSON an
|
|
|
49
49
|
- **NEVER** call `.join()` on arrays without checking element types — object elements produce `[object Object]`
|
|
50
50
|
- **Final file** MUST be > 100KB
|
|
51
51
|
- **Final file** MUST NOT contain the string `[object Object]`
|
|
52
|
+
- **NEVER** reuse a build script (`_build-html.js`, `generate-html.js`) from a previous session — always re-execute steps 01→02→03→04 from scratch. Build scripts are throwaway artifacts that bypass the skill's normalization logic.
|
|
53
|
+
- **With `--force`:** delete any existing build script before proceeding
|
|
52
54
|
|
|
53
55
|
## References
|
|
54
56
|
|
|
@@ -4663,16 +4663,43 @@ function renderSmartDashboardMockup(res) {
|
|
|
4663
4663
|
{ label: 'Taux', value: '80%' }
|
|
4664
4664
|
];
|
|
4665
4665
|
kpis.forEach(function(kpi) {
|
|
4666
|
-
|
|
4666
|
+
var displayValue = kpiDisplayValue(kpi);
|
|
4667
|
+
html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(displayValue) + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
|
|
4667
4668
|
});
|
|
4668
4669
|
html += '</div>';
|
|
4669
4670
|
|
|
4670
|
-
// Chart
|
|
4671
|
-
|
|
4671
|
+
// Chart placeholders
|
|
4672
|
+
var charts = res.charts || [{ label: 'Graphique' }];
|
|
4673
|
+
charts.forEach(function(chart) {
|
|
4674
|
+
html += '<div class="mock-chart-placeholder">' + escapeHtml(chart.label || chart.type || 'Graphique') + '</div>';
|
|
4675
|
+
});
|
|
4672
4676
|
|
|
4673
4677
|
return html;
|
|
4674
4678
|
}
|
|
4675
4679
|
|
|
4680
|
+
function kpiDisplayValue(kpi) {
|
|
4681
|
+
var v = kpi.value || '0';
|
|
4682
|
+
// If value is already a simple display value (number, percentage), use it
|
|
4683
|
+
if (/^\d[\d,.\s]*%?$/.test(v)) return v;
|
|
4684
|
+
// Detect formula patterns and generate realistic sample values
|
|
4685
|
+
if (v.indexOf('(') !== -1) {
|
|
4686
|
+
var label = (kpi.label || '').toLowerCase();
|
|
4687
|
+
var color = (kpi.color || '').toLowerCase();
|
|
4688
|
+
// Percentage indicators
|
|
4689
|
+
if (v.indexOf('ratio') !== -1 || v.indexOf('%') !== -1 || label.indexOf('taux') !== -1 || label.indexOf('rate') !== -1)
|
|
4690
|
+
return '87.5%';
|
|
4691
|
+
// Average
|
|
4692
|
+
if (v.indexOf('avg') !== -1 || label.indexOf('moyen') !== -1 || label.indexOf('average') !== -1)
|
|
4693
|
+
return '12.4';
|
|
4694
|
+
// Sum (monetary or hours)
|
|
4695
|
+
if (v.indexOf('sum') !== -1)
|
|
4696
|
+
return label.indexOf('jour') !== -1 || label.indexOf('day') !== -1 ? '156' : '45,678';
|
|
4697
|
+
// Count (default)
|
|
4698
|
+
return '1,234';
|
|
4699
|
+
}
|
|
4700
|
+
return v;
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4676
4703
|
/* ---------- SmartFilter ---------- */
|
|
4677
4704
|
function renderSmartFilterMockup(res) {
|
|
4678
4705
|
var options = res.options || [];
|
|
@@ -303,16 +303,43 @@ function renderSmartDashboardMockup(res) {
|
|
|
303
303
|
{ label: 'Taux', value: '80%' }
|
|
304
304
|
];
|
|
305
305
|
kpis.forEach(function(kpi) {
|
|
306
|
-
|
|
306
|
+
var displayValue = kpiDisplayValue(kpi);
|
|
307
|
+
html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(displayValue) + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
|
|
307
308
|
});
|
|
308
309
|
html += '</div>';
|
|
309
310
|
|
|
310
|
-
// Chart
|
|
311
|
-
|
|
311
|
+
// Chart placeholders
|
|
312
|
+
var charts = res.charts || [{ label: 'Graphique' }];
|
|
313
|
+
charts.forEach(function(chart) {
|
|
314
|
+
html += '<div class="mock-chart-placeholder">' + escapeHtml(chart.label || chart.type || 'Graphique') + '</div>';
|
|
315
|
+
});
|
|
312
316
|
|
|
313
317
|
return html;
|
|
314
318
|
}
|
|
315
319
|
|
|
320
|
+
function kpiDisplayValue(kpi) {
|
|
321
|
+
var v = kpi.value || '0';
|
|
322
|
+
// If value is already a simple display value (number, percentage), use it
|
|
323
|
+
if (/^\d[\d,.\s]*%?$/.test(v)) return v;
|
|
324
|
+
// Detect formula patterns and generate realistic sample values
|
|
325
|
+
if (v.indexOf('(') !== -1) {
|
|
326
|
+
var label = (kpi.label || '').toLowerCase();
|
|
327
|
+
var color = (kpi.color || '').toLowerCase();
|
|
328
|
+
// Percentage indicators
|
|
329
|
+
if (v.indexOf('ratio') !== -1 || v.indexOf('%') !== -1 || label.indexOf('taux') !== -1 || label.indexOf('rate') !== -1)
|
|
330
|
+
return '87.5%';
|
|
331
|
+
// Average
|
|
332
|
+
if (v.indexOf('avg') !== -1 || label.indexOf('moyen') !== -1 || label.indexOf('average') !== -1)
|
|
333
|
+
return '12.4';
|
|
334
|
+
// Sum (monetary or hours)
|
|
335
|
+
if (v.indexOf('sum') !== -1)
|
|
336
|
+
return label.indexOf('jour') !== -1 || label.indexOf('day') !== -1 ? '156' : '45,678';
|
|
337
|
+
// Count (default)
|
|
338
|
+
return '1,234';
|
|
339
|
+
}
|
|
340
|
+
return v;
|
|
341
|
+
}
|
|
342
|
+
|
|
316
343
|
/* ---------- SmartFilter ---------- */
|
|
317
344
|
function renderSmartFilterMockup(res) {
|
|
318
345
|
var options = res.options || [];
|
|
@@ -192,6 +192,29 @@ function normalizeAction(a) {
|
|
|
192
192
|
return a.code || a.id || a.label || a.actionCode || "";
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// Helper: normalize a KPI — replace formula values with sample display values
|
|
196
|
+
function normalizeKpi(k) {
|
|
197
|
+
if (typeof k !== 'object') return { label: String(k), value: "0" };
|
|
198
|
+
const label = k.label || k.displayName || "";
|
|
199
|
+
const rawValue = k.value || k.metric || "0";
|
|
200
|
+
let displayValue = rawValue;
|
|
201
|
+
// Detect formula patterns (e.g., count(...), sum(...), ratio(...), avg(...))
|
|
202
|
+
if (typeof rawValue === 'string' && rawValue.includes('(')) {
|
|
203
|
+
const lbl = label.toLowerCase();
|
|
204
|
+
if (rawValue.includes('ratio') || rawValue.includes('%') || lbl.includes('taux') || lbl.includes('rate'))
|
|
205
|
+
displayValue = "87.5%";
|
|
206
|
+
else if (rawValue.includes('avg') || lbl.includes('moyen') || lbl.includes('average'))
|
|
207
|
+
displayValue = "12.4";
|
|
208
|
+
else if (rawValue.includes('sum'))
|
|
209
|
+
displayValue = (lbl.includes('jour') || lbl.includes('day') || lbl.includes('heure') || lbl.includes('hour')) ? "156" : "45,678";
|
|
210
|
+
else
|
|
211
|
+
displayValue = "1,234";
|
|
212
|
+
}
|
|
213
|
+
// Also handle format hint from the KPI definition
|
|
214
|
+
if (k.format === 'percentage' && displayValue === rawValue) displayValue = "80%";
|
|
215
|
+
return { label: label, value: displayValue };
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
if (flatScr.length > 0) {
|
|
196
219
|
// FORMAT A: flat screens[] array (ba-003 style)
|
|
197
220
|
const bySec = {};
|
|
@@ -214,9 +237,7 @@ if (flatScr.length > 0) {
|
|
|
214
237
|
fields: (t.fields || []).map(normalizeField)
|
|
215
238
|
})),
|
|
216
239
|
actions: (s.actions || []).map(normalizeAction),
|
|
217
|
-
kpis: (s.kpis || []).map(
|
|
218
|
-
? { label: k.label || k.displayName || "", value: k.value || k.metric || "0" }
|
|
219
|
-
: { label: String(k), value: "0" }),
|
|
240
|
+
kpis: (s.kpis || []).map(normalizeKpi),
|
|
220
241
|
options: s.options || [],
|
|
221
242
|
permission: s.permission || "",
|
|
222
243
|
notes: s.description || s.sectionDescription || ""
|
|
@@ -224,39 +245,65 @@ if (flatScr.length > 0) {
|
|
|
224
245
|
});
|
|
225
246
|
screens = Object.values(bySec);
|
|
226
247
|
} else if (sectionScr.length > 0) {
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
248
|
+
// DETECT sub-format: canonical (resources[] inside sections) vs ba-004 (data on section itself)
|
|
249
|
+
const hasNestedResources = sectionScr.some(sec => (sec.resources || []).length > 0);
|
|
250
|
+
|
|
251
|
+
if (hasNestedResources) {
|
|
252
|
+
// FORMAT B-CANONICAL: sections[] with resources[] (ba-005+ style — sectionCode/sectionLabel/resources[])
|
|
253
|
+
// Resources have their own code/type/columns/tabs/kpis — iterate over them
|
|
254
|
+
screens = sectionScr.map(sec => ({
|
|
255
|
+
sectionCode: sec.sectionCode || sec.id || sec.code || "",
|
|
256
|
+
sectionLabel: sec.sectionLabel || sec.displayName || sec.label || sec.sectionCode || "",
|
|
257
|
+
resources: (sec.resources || []).map(res => ({
|
|
258
|
+
code: res.code || res.id || "",
|
|
259
|
+
label: res.label || res.displayName || res.code || "",
|
|
260
|
+
type: res.type || res.componentType || res.layout || "unknown",
|
|
261
|
+
columns: (res.columns || []).map(normalizeColumn),
|
|
262
|
+
filters: (res.filters || []).map(normalizeFilter),
|
|
263
|
+
fields: [],
|
|
264
|
+
tabs: (res.tabs || []).map(t => ({
|
|
265
|
+
label: t.label || t.displayName || t.code || t.id || "",
|
|
266
|
+
fields: (t.fields || []).map(normalizeField)
|
|
267
|
+
})),
|
|
268
|
+
actions: (res.actions || []).map(normalizeAction),
|
|
269
|
+
kpis: (res.kpis || []).map(normalizeKpi),
|
|
270
|
+
options: res.options || [],
|
|
271
|
+
permission: res.permission || "",
|
|
272
|
+
notes: res.notes || res.description || ""
|
|
273
|
+
}))
|
|
274
|
+
}));
|
|
275
|
+
} else {
|
|
276
|
+
// FORMAT B-LEGACY: sections[] without resources[] (ba-004 style — data directly on section)
|
|
277
|
+
const bySec = {};
|
|
278
|
+
sectionScr.forEach(sec => {
|
|
279
|
+
const secId = sec.id || sec.code || sec.sectionCode || "";
|
|
280
|
+
const secLabel = sec.displayName || sec.label || sec.sectionLabel || secId;
|
|
281
|
+
const componentType = sec.layout || sec.componentType || "unknown";
|
|
282
|
+
if (!bySec[secId]) bySec[secId] = {
|
|
283
|
+
sectionCode: secId,
|
|
284
|
+
sectionLabel: secLabel,
|
|
285
|
+
resources: []
|
|
286
|
+
};
|
|
287
|
+
bySec[secId].resources.push({
|
|
288
|
+
code: secId,
|
|
289
|
+
label: secLabel,
|
|
290
|
+
type: componentType,
|
|
291
|
+
columns: (sec.columns || []).map(normalizeColumn),
|
|
292
|
+
filters: (sec.filters || []).map(normalizeFilter),
|
|
293
|
+
fields: [],
|
|
294
|
+
tabs: (sec.tabs || []).map(t => ({
|
|
295
|
+
label: t.displayName || t.label || t.code || t.id || "",
|
|
296
|
+
fields: (t.fields || []).map(normalizeField)
|
|
297
|
+
})),
|
|
298
|
+
actions: (sec.actions || []).map(normalizeAction),
|
|
299
|
+
kpis: (sec.kpis || []).map(normalizeKpi),
|
|
300
|
+
options: sec.options || [],
|
|
301
|
+
permission: (typeof sec.permissions === 'object' ? sec.permissions?.view : sec.permission) || "",
|
|
302
|
+
notes: sec.description || ""
|
|
303
|
+
});
|
|
257
304
|
});
|
|
258
|
-
|
|
259
|
-
|
|
305
|
+
screens = Object.values(bySec);
|
|
306
|
+
}
|
|
260
307
|
}
|
|
261
308
|
moduleSpecs[moduleCode].screens = screens;
|
|
262
309
|
```
|
|
@@ -13,6 +13,23 @@ Inject FEATURE_DATA and EMBEDDED_ARTIFACTS into the HTML template and write the
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## PRE-RENDER CLEANUP (MANDATORY)
|
|
17
|
+
|
|
18
|
+
> **NEVER reuse a build script from a previous session.**
|
|
19
|
+
> Build scripts (`_build-html.js`, `generate-html.js`) are throwaway artifacts that bypass
|
|
20
|
+
> the skill's normalization logic (normalizeKpi, normalizeColumn, Format B-CANONICAL).
|
|
21
|
+
> Reusing them causes stale data and missed fixes.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Delete any existing build scripts in the project directory:
|
|
25
|
+
rm -f _build-html.js generate-html.js build-html.js
|
|
26
|
+
|
|
27
|
+
This ensures the HTML is always generated from the current step-02 output,
|
|
28
|
+
not from a cached script that may use outdated mapping logic.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
16
33
|
## EXECUTION SEQUENCE
|
|
17
34
|
|
|
18
35
|
### 1. Locate Template
|