@atlashub/smartstack-cli 4.53.0 → 4.54.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.53.0",
3
+ "version": "4.54.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -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:
@@ -4663,16 +4663,43 @@ function renderSmartDashboardMockup(res) {
4663
4663
  { label: 'Taux', value: '80%' }
4664
4664
  ];
4665
4665
  kpis.forEach(function(kpi) {
4666
- html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(kpi.value || '0') + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
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 placeholder
4671
- html += '<div class="mock-chart-placeholder">Graphique</div>';
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
- html += '<div class="mock-kpi"><div class="mock-kpi-value">' + escapeHtml(kpi.value || '0') + '</div><div class="mock-kpi-label">' + escapeHtml(kpi.label || '') + '</div></div>';
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 placeholder
311
- html += '<div class="mock-chart-placeholder">Graphique</div>';
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(k => typeof k === 'object'
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
- // FORMAT B: sections[] array (ba-004 style id/displayName/layout convention)
228
- // Each section IS a screen resource (columns, tabs, filters are ON the section itself)
229
- const bySec = {};
230
- sectionScr.forEach(sec => {
231
- const secId = sec.id || sec.code || sec.sectionCode || "";
232
- const secLabel = sec.displayName || sec.label || sec.sectionLabel || secId;
233
- const componentType = sec.layout || sec.componentType || "unknown";
234
- if (!bySec[secId]) bySec[secId] = {
235
- sectionCode: secId,
236
- sectionLabel: secLabel,
237
- resources: []
238
- };
239
- bySec[secId].resources.push({
240
- code: secId,
241
- label: secLabel,
242
- type: componentType,
243
- columns: (sec.columns || []).map(normalizeColumn),
244
- filters: (sec.filters || []).map(normalizeFilter),
245
- fields: [],
246
- tabs: (sec.tabs || []).map(t => ({
247
- label: t.displayName || t.label || t.code || t.id || "",
248
- fields: (t.fields || []).map(normalizeField)
249
- })),
250
- actions: (sec.actions || []).map(normalizeAction),
251
- kpis: (sec.kpis || []).map(k => typeof k === 'object'
252
- ? { label: k.displayName || k.label || "", value: k.format === "percentage" ? "80%" : "1,234" }
253
- : { label: String(k), value: "0" }),
254
- options: sec.options || [],
255
- permission: (typeof sec.permissions === 'object' ? sec.permissions?.view : sec.permission) || "",
256
- notes: sec.description || ""
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
- screens = Object.values(bySec);
305
+ screens = Object.values(bySec);
306
+ }
260
307
  }
261
308
  moduleSpecs[moduleCode].screens = screens;
262
309
  ```