@atlashub/smartstack-cli 4.75.0 → 4.76.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 (54) hide show
  1. package/dist/index.js +87 -41
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/skills/apex/SKILL.md +2 -2
  5. package/templates/skills/apex/references/checks/frontend-checks.sh +26 -0
  6. package/templates/skills/apex/references/checks/seed-checks.sh +47 -7
  7. package/templates/skills/apex/references/core-seed-data.md +20 -18
  8. package/templates/skills/apex/references/post-checks.md +18 -1
  9. package/templates/skills/apex/references/smartstack-api.md +4 -4
  10. package/templates/skills/apex/references/smartstack-frontend.md +1 -1
  11. package/templates/skills/apex/references/smartstack-layers.md +6 -6
  12. package/templates/skills/apex/steps/step-00-init.md +1 -1
  13. package/templates/skills/apex/steps/step-03b-layer1-seed.md +26 -0
  14. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +124 -2
  15. package/templates/skills/apex/steps/step-04-examine.md +163 -0
  16. package/templates/skills/apex-verify/SKILL.md +110 -0
  17. package/templates/skills/apex-verify/references/audit-rules.md +50 -0
  18. package/templates/skills/apex-verify/steps/step-00-init.md +119 -0
  19. package/templates/skills/apex-verify/steps/step-01-nav-audit.md +92 -0
  20. package/templates/skills/apex-verify/steps/step-02-crud-audit.md +127 -0
  21. package/templates/skills/apex-verify/steps/step-03-perm-audit.md +119 -0
  22. package/templates/skills/apex-verify/steps/step-04-route-audit.md +98 -0
  23. package/templates/skills/apex-verify/steps/step-05-report.md +110 -0
  24. package/templates/skills/application/templates-frontend.md +2 -2
  25. package/templates/skills/business-analyse/SKILL.md +3 -3
  26. package/templates/skills/business-analyse/_shared.md +37 -0
  27. package/templates/skills/business-analyse/references/03-json-schemas.md +11 -3
  28. package/templates/skills/business-analyse/references/03-post-check-validation.md +64 -0
  29. package/templates/skills/business-analyse/references/canonical-json-formats.md +7 -3
  30. package/templates/skills/business-analyse/references/robustness-checks.md +1 -1
  31. package/templates/skills/business-analyse/references/validation-checklist.md +5 -5
  32. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +15 -4
  33. package/templates/skills/business-analyse/steps/step-03-specify.md +162 -4
  34. package/templates/skills/business-analyse/steps/step-04-consolidate.md +211 -1
  35. package/templates/skills/business-analyse-handoff/references/agent-handoff-transform-prompt.md +3 -0
  36. package/templates/skills/business-analyse-html/html/ba-interactive.html +198 -16
  37. package/templates/skills/business-analyse-html/html/src/scripts/01-data-init.js +64 -0
  38. package/templates/skills/business-analyse-html/html/src/scripts/05-render-specs.js +80 -11
  39. package/templates/skills/business-analyse-html/html/src/scripts/06-render-consolidation.js +2 -2
  40. package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +6 -3
  41. package/templates/skills/business-analyse-html/html/src/scripts/12-render-diagrams.js +46 -0
  42. package/templates/skills/business-analyse-html/references/02-feature-data-building.md +4 -2
  43. package/templates/skills/business-analyse-html/references/data-build.md +2 -0
  44. package/templates/skills/business-analyse-html/references/data-mapping.md +88 -21
  45. package/templates/skills/business-analyse-html/steps/step-02-build-data.md +6 -0
  46. package/templates/skills/business-analyse-html/steps/step-04-verify.md +92 -3
  47. package/templates/skills/business-analyse-quick/SKILL.md +807 -0
  48. package/templates/skills/{sketch → business-analyse-quick}/references/domain-heuristics.md +59 -3
  49. package/templates/skills/business-analyse-quick/references/prd-schema.md +268 -0
  50. package/templates/skills/business-analyse-review/references/review-data-mapping.md +6 -0
  51. package/templates/skills/dev-start/SKILL.md +7 -7
  52. package/templates/skills/sketch/SKILL.md +15 -153
  53. package/templates/skills/ui-components/SKILL.md +1 -1
  54. package/templates/skills/ui-components/patterns/data-table.md +1 -1
@@ -24,6 +24,24 @@ function renderMermaidDiagrams() {
24
24
  }
25
25
  }
26
26
 
27
+ // 1b. MCD (Modèle Conceptuel de Données) — after ERD
28
+ if (diagrams.mcd) {
29
+ var erdContainer = document.getElementById('dataModelContainer');
30
+ if (erdContainer) {
31
+ var mcdDiv = document.createElement('div');
32
+ mcdDiv.className = 'diagram-container diagram-mcd';
33
+ mcdDiv.innerHTML =
34
+ '<div class="diagram-section-header" style="font-size:0.95rem;font-weight:600;color:var(--text-bright);margin-bottom:0.75rem;">Mod\u00e8le Conceptuel de Donn\u00e9es (MCD)</div>' +
35
+ '<div class="mermaid">' + escapeHtml(diagrams.mcd) + '</div>';
36
+ var erdDiv = erdContainer.querySelector('.diagram-erd');
37
+ if (erdDiv && erdDiv.nextSibling) {
38
+ erdContainer.insertBefore(mcdDiv, erdDiv.nextSibling);
39
+ } else {
40
+ erdContainer.appendChild(mcdDiv);
41
+ }
42
+ }
43
+ }
44
+
27
45
  // 2. State machine diagrams — inject in module spec sections or consol-datamodel
28
46
  if (diagrams.stateMachines && Object.keys(diagrams.stateMachines).length > 0) {
29
47
  const smContainer = document.getElementById('dataModelContainer');
@@ -46,6 +64,34 @@ function renderMermaidDiagrams() {
46
64
  }
47
65
  }
48
66
 
67
+ // 2b. Use Case diagrams — inject in each module's UC tab
68
+ if (diagrams.useCases && Object.keys(diagrams.useCases).length > 0) {
69
+ Object.entries(diagrams.useCases).forEach(function(entry) {
70
+ var moduleCode = entry[0];
71
+ var def = entry[1];
72
+ var ucTab = document.getElementById('tab-' + moduleCode + '-uc');
73
+ if (!ucTab) {
74
+ // Try kebab-case version
75
+ var kebab = moduleCode.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
76
+ ucTab = document.getElementById('tab-' + kebab + '-uc');
77
+ }
78
+ if (ucTab) {
79
+ var ucDiagramDiv = document.createElement('div');
80
+ ucDiagramDiv.className = 'diagram-container diagram-usecase';
81
+ ucDiagramDiv.innerHTML =
82
+ '<div class="diagram-section-header" style="font-size:0.95rem;font-weight:600;color:var(--text-bright);margin-bottom:0.75rem;">Diagramme de cas d\'utilisation</div>' +
83
+ '<div class="mermaid">' + escapeHtml(def) + '</div>';
84
+ // Insert after the first paragraph (description text)
85
+ var firstP = ucTab.querySelector('p');
86
+ if (firstP && firstP.nextSibling) {
87
+ ucTab.insertBefore(ucDiagramDiv, firstP.nextSibling);
88
+ } else {
89
+ ucTab.insertBefore(ucDiagramDiv, ucTab.firstChild);
90
+ }
91
+ }
92
+ });
93
+ }
94
+
49
95
  // 3. Sequence diagrams in consol-flows
50
96
  if (diagrams.sequences && Object.keys(diagrams.sequences).length > 0) {
51
97
  var flowsContainer = document.getElementById('consolFlowsContainer');
@@ -36,10 +36,12 @@ moduleSpecs[moduleCode] = {
36
36
  name: br.name || br.id || "",
37
37
  sectionCode: br.sectionCode || "",
38
38
  category: br.category || "",
39
+ severity: br.severity || "",
39
40
  statement: br.statement || br.description || "",
40
- example: (br.examples || []).map(e =>
41
+ example: (br.examples || (br.example ? [br.example] : [])).map(e =>
41
42
  typeof e === 'string' ? e : ((e.input || "") + " → " + (e.expected || ""))
42
- ).join("; ")
43
+ ).join("; "),
44
+ domainSpecific: br.domainSpecific || false
43
45
  })),
44
46
  // ENTITY SAFETY NET: map fields[] → attributes[] if agent deviated
45
47
  entities: (mod.entities?.entities || []).map(ent => ({
@@ -133,6 +133,8 @@ for rendering in the HTML viewer.
133
133
  ```javascript
134
134
  mermaidDiagrams: {
135
135
  erd: consolidation.mermaidDiagrams?.erd || null, // ERD string or null
136
+ mcd: consolidation.mermaidDiagrams?.mcd || null, // MCD string or null
137
+ useCases: consolidation.mermaidDiagrams?.useCases || {}, // {moduleCode: mermaid_def}
136
138
  stateMachines: consolidation.mermaidDiagrams?.stateMachines || {}, // {entity: mermaid_def}
137
139
  sequences: consolidation.mermaidDiagrams?.sequences || {} // {flowName: mermaid_def}
138
140
  }
@@ -122,6 +122,7 @@ moduleSpecs[moduleCode] = {
122
122
  name: uc.name || uc.title || uc.id || "", // Format A: "name", Format B: "title" or "id"
123
123
  sectionCode: uc.sectionCode || "",
124
124
  actor: uc.primaryActor || uc.actor || "", // Format A: "primaryActor", Format B: "actor"
125
+ description: uc.description || "", // Optional description field
125
126
  // SAFETY NET: steps may be string[] or object[] ({step, action})
126
127
  steps: (uc.mainScenario || uc.steps || []).map(s =>
127
128
  typeof s === 'string' ? s : (s.action || s.description || "")
@@ -130,7 +131,10 @@ moduleSpecs[moduleCode] = {
130
131
  (a.name || a.trigger || "") + ": " + (a.steps || a.actions || []).map(s =>
131
132
  typeof s === 'string' ? s : (s.action || s.description || "")
132
133
  ).join(", ")
133
- ).join("\n")
134
+ ).join("\n"),
135
+ preconditions: uc.preconditions || [], // Array of precondition objects or strings
136
+ postconditions: uc.postconditions || [], // Array of postcondition objects or strings
137
+ errorScenarios: uc.errorScenarios || [] // Array of error scenario objects
134
138
  })),
135
139
  businessRules: rawBRs.map(br => ({
136
140
  name: br.name || br.id || "",
@@ -166,19 +170,28 @@ The HTML uses `"Role|Action"` format (e.g. `"RH Admin|Consulter"`):
166
170
 
167
171
  ```javascript
168
172
  // Input: permissions object from flat file (mod.permissions), NOT moduleFeature
169
- function buildPermissionKeys(permissionsData) {
173
+ // lang: optional language override ('fr' or 'en'), defaults to 'fr'
174
+ function buildPermissionKeys(permissionsData, lang = 'fr') {
170
175
  const keys = [];
171
- const actionMap = {
172
- Read: "Consulter", Create: "Créer", Update: "Modifier",
173
- Delete: "Supprimer", Validate: "Valider", Export: "Exporter",
174
- Submit: "Valider", Import: "Créer",
175
- read: "Consulter", create: "Créer", update: "Modifier",
176
- delete: "Supprimer", validate: "Valider", export: "Exporter",
177
- submit: "Valider", import: "Créer"
178
- };
176
+
177
+ // Bilingual actionMap based on language parameter
178
+ const actionMap = lang === 'en'
179
+ ? {
180
+ read: 'Read', create: 'Create', update: 'Update', delete: 'Delete',
181
+ approve: 'Approve', export: 'Export', validate: 'Validate', admin: 'Admin',
182
+ Read: 'Read', Create: 'Create', Update: 'Update', Delete: 'Delete',
183
+ Approve: 'Approve', Export: 'Export', Validate: 'Validate', Admin: 'Admin',
184
+ submit: 'Validate', import: 'Create', Submit: 'Validate', Import: 'Create'
185
+ }
186
+ : {
187
+ read: 'Consulter', create: 'Créer', update: 'Modifier', delete: 'Supprimer',
188
+ approve: 'Valider', export: 'Exporter', validate: 'Valider', admin: 'Administrer',
189
+ Read: 'Consulter', Create: 'Créer', Update: 'Modifier', Delete: 'Supprimer',
190
+ Approve: 'Valider', Export: 'Exporter', Validate: 'Valider', Admin: 'Administrer',
191
+ submit: 'Valider', import: 'Créer', Submit: 'Valider', Import: 'Créer'
192
+ };
179
193
 
180
194
  const matrix = permissionsData?.matrix || permissionsData?.permissionMatrix;
181
- if (!matrix) return keys;
182
195
 
183
196
  // Format A: matrix is array of { role, permissions[] } (ba-009+ format)
184
197
  if (Array.isArray(matrix)) {
@@ -202,25 +215,79 @@ function buildPermissionKeys(permissionsData) {
202
215
  }
203
216
 
204
217
  // Format B: legacy matrix.roleAssignments[] format
205
- (matrix.roleAssignments || []).forEach(ra => {
206
- (ra.permissions || []).forEach(permPath => {
207
- const action = permPath.split(".").pop();
208
- if (action === "*") {
209
- Object.values(actionMap).forEach(uiAction => {
218
+ if (matrix && matrix.roleAssignments) {
219
+ (matrix.roleAssignments || []).forEach(ra => {
220
+ (ra.permissions || []).forEach(permPath => {
221
+ const action = permPath.split(".").pop();
222
+ if (action === "*") {
223
+ Object.values(actionMap).forEach(uiAction => {
224
+ const key = ra.role + "|" + uiAction;
225
+ if (!keys.includes(key)) keys.push(key);
226
+ });
227
+ } else {
228
+ const uiAction = actionMap[action] || action;
210
229
  const key = ra.role + "|" + uiAction;
211
230
  if (!keys.includes(key)) keys.push(key);
231
+ }
232
+ });
233
+ });
234
+ return keys;
235
+ }
236
+
237
+ // Format C: permissions.json from step-03 with top-level roles[]
238
+ if (keys.length === 0 && permissionsData?.roles) {
239
+ const actionMap_C = lang === 'en'
240
+ ? { read: 'Read', create: 'Create', update: 'Update', delete: 'Delete', approve: 'Approve', export: 'Export', validate: 'Validate', admin: 'Admin' }
241
+ : { read: 'Consulter', create: 'Créer', update: 'Modifier', delete: 'Supprimer', approve: 'Valider', export: 'Exporter', validate: 'Valider', admin: 'Administrer' };
242
+
243
+ permissionsData.roles.forEach(entry => {
244
+ const role = entry.role || entry.name || '';
245
+ (entry.permissions || entry.actions || []).forEach(perm => {
246
+ const action = typeof perm === 'string' ? perm.split('.').pop().toLowerCase() : (perm.action || '').toLowerCase();
247
+ const uiAction = actionMap_C[action] || action;
248
+ const key = role + '|' + uiAction;
249
+ if (role && !keys.includes(key)) keys.push(key);
250
+ });
251
+ // Handle wildcard patterns like "HumanResources.*"
252
+ if ((entry.permissionPattern || '').endsWith('*')) {
253
+ Object.values(actionMap_C).forEach(a => {
254
+ const key = (entry.role || entry.name) + '|' + a;
255
+ if (!keys.includes(key)) keys.push(key);
212
256
  });
213
- } else {
214
- const uiAction = actionMap[action] || action;
215
- keys.push(ra.role + "|" + uiAction);
216
257
  }
217
258
  });
218
- });
259
+ }
260
+
261
+ // Format D: permissionMatrix.roleAssignments[]
262
+ if (keys.length === 0 && permissionsData?.permissionMatrix?.roleAssignments) {
263
+ const actionMap_D = lang === 'en'
264
+ ? { read: 'Read', create: 'Create', update: 'Update', delete: 'Delete', approve: 'Approve', export: 'Export', validate: 'Validate', admin: 'Admin' }
265
+ : { read: 'Consulter', create: 'Créer', update: 'Modifier', delete: 'Supprimer', approve: 'Valider', export: 'Exporter', validate: 'Valider', admin: 'Administrer' };
266
+
267
+ permissionsData.permissionMatrix.roleAssignments.forEach(entry => {
268
+ const role = entry.role || '';
269
+ (entry.permissions || []).forEach(perm => {
270
+ if (typeof perm === 'string' && perm.endsWith('.*')) {
271
+ // Wildcard: expand to all standard actions
272
+ Object.values(actionMap_D).forEach(a => {
273
+ const key = role + '|' + a;
274
+ if (!keys.includes(key)) keys.push(key);
275
+ });
276
+ } else {
277
+ const action = typeof perm === 'string' ? perm.split('.').pop().toLowerCase() : '';
278
+ const uiAction = actionMap_D[action] || action;
279
+ const key = role + '|' + uiAction;
280
+ if (role && action && !keys.includes(key)) keys.push(key);
281
+ }
282
+ });
283
+ });
284
+ }
285
+
219
286
  return keys;
220
287
  }
221
288
  ```
222
289
 
223
- > **Language handling:** The actionMap should use `metadata.language` to select the correct labels. For `fr`: Consulter/Créer/Modifier/Supprimer. For `en`: Read/Create/Update/Delete. Default to English if language is unknown.
290
+ > **Language handling:** The function accepts an optional `lang` parameter ('fr' or 'en'). Defaults to 'fr' (French). For English labels: Read/Create/Update/Delete/Export/Validate. For French: Consulter/Créer/Modifier/Supprimer/Exporter/Valider.
224
291
 
225
292
  ### Frequency Mapping
226
293
 
@@ -64,6 +64,12 @@ This step is NEVER blocking — ASCII-only wireframes are acceptable.
64
64
  - Wireframe fields use format/content (NOT mockupFormat/mockup)
65
65
  - Per-module: useCases/businessRules/entities count must match source (empty when source has data = BUG)
66
66
  - dependencies[] must be present (even if empty) to prevent HTML crashes
67
+ - **Permissions:** `buildPermissionKeys()` supports 4 formats:
68
+ - **Format A:** `permissions.matrix[]` with `{role, permissions[]}`
69
+ - **Format B:** `permissions.permissionMatrix.roleAssignments[]`
70
+ - **Format C:** `permissions.roles[]` with `{role, permissions[]}`
71
+ - **Format D:** `permissions.permissionMatrix.roleAssignments[]` with wildcard expansion
72
+ - Pass optional `lang` parameter ('fr'|'en') for bilingual labels. Defaults to 'fr'.
67
73
 
68
74
  ## NEXT STEP
69
75
 
@@ -8,7 +8,7 @@ model: opus
8
8
 
9
9
  ## YOUR TASK
10
10
 
11
- Run 9 blocking validations on the generated HTML file and display the completion summary.
11
+ Run 16 blocking validations on the generated HTML file and display the completion summary.
12
12
 
13
13
  ---
14
14
 
@@ -105,11 +105,97 @@ IF "modules" is missing:
105
105
  BLOCKING_ERROR("FEATURE_DATA.modules missing — page will crash")
106
106
  ```
107
107
 
108
- > **IF any check fails:** fix the issue and re-run the failing step before completing.
108
+ > **IF any check fails (1-9):** fix the issue and re-run the failing step before completing.
109
+
110
+ ### 2. Content Quality Validations (NEW)
111
+
112
+ **Check 10 — Language coherence:**
113
+ ```
114
+ Read FEATURE_DATA.metadata.language (or config.json language field)
115
+ IF language is set (e.g., "fr"):
116
+ FOR each module in FEATURE_DATA.modules:
117
+ const spec = FEATURE_DATA.moduleSpecs[module.code]
118
+
119
+ // Check UC names
120
+ FOR each uc in spec.useCases:
121
+ IF language == "fr" AND uc.name matches /^(Create|Update|Delete|View|List|Manage|Submit|Approve|Reject|Export)\b/:
122
+ WARNING("Module {module.code}: UC '{uc.name}' appears English (expected French)")
123
+
124
+ // Check BR statements
125
+ FOR each br in spec.businessRules:
126
+ IF language == "fr" AND br.statement matches /\b(must|should|cannot|when|if the|is required)\b/i:
127
+ WARNING("Module {module.code}: BR '{br.name}' statement appears English (expected French)")
128
+
129
+ Count total warnings. IF > 20% of items are in wrong language:
130
+ WARNING("⚠ Significant language mismatch: {count} items in wrong language")
131
+ ```
132
+
133
+ **Check 11 — Business rules have examples:**
134
+ ```
135
+ FOR each module in FEATURE_DATA.modules:
136
+ const brs = FEATURE_DATA.moduleSpecs[module.code].businessRules || []
137
+ IF brs.length === 0: SKIP
138
+ const withExamples = brs.filter(br => br.example && br.example.trim() !== '')
139
+ const rate = withExamples.length / brs.length
140
+ IF rate < 0.5:
141
+ WARNING("Module {module.code}: only {withExamples.length}/{brs.length} BRs have examples ({rate*100}%)")
142
+ ```
143
+
144
+ **Check 12 — Entity attributes have type field:**
145
+ ```
146
+ FOR each module in FEATURE_DATA.modules:
147
+ const entities = FEATURE_DATA.moduleSpecs[module.code].entities || []
148
+ FOR each entity in entities:
149
+ const attrs = entity.attributes || []
150
+ const withType = attrs.filter(a => a.type && a.type.trim() !== '' && a.type !== 'string')
151
+ IF attrs.length > 3 AND withType.length === 0:
152
+ WARNING("Module {module.code}: entity '{entity.name}' — all {attrs.length} attributes default to 'string' (types may be missing)")
153
+ ```
154
+
155
+ **Check 13 — Permissions populated per module:**
156
+ ```
157
+ FOR each module in FEATURE_DATA.modules:
158
+ const perms = FEATURE_DATA.moduleSpecs[module.code].permissions || []
159
+ IF perms.length === 0:
160
+ WARNING("Module {module.code}: 0 permissions — permission grid will be empty")
161
+ ```
162
+
163
+ **Check 14 — Mockups not empty (screens with actual resources):**
164
+ ```
165
+ FOR each module in FEATURE_DATA.modules:
166
+ const screens = FEATURE_DATA.moduleSpecs[module.code].screens || []
167
+ const wireframes = EMBEDDED_ARTIFACTS.wireframes?.[module.code] || []
168
+ IF screens.length === 0 AND wireframes.length === 0:
169
+ WARNING("Module {module.code}: no screens and no wireframes — Mockups tab will show 'Aucune maquette'")
170
+ ELSE IF screens.length > 0:
171
+ const totalResources = screens.reduce((sum, s) => sum + (s.resources || []).length, 0)
172
+ IF totalResources === 0:
173
+ WARNING("Module {module.code}: has screens[] but 0 resources — mockups will appear empty despite data existing")
174
+ ```
175
+
176
+ **Check 15 — MCD diagram present (multi-module projects):**
177
+ ```
178
+ const diagrams = FEATURE_DATA.consolidation?.mermaidDiagrams || {}
179
+ IF FEATURE_DATA.modules.length > 1:
180
+ IF !diagrams.erd:
181
+ WARNING("No ERD diagram — data model section will have no visual")
182
+ IF !diagrams.mcd:
183
+ WARNING("No MCD diagram — conceptual model not generated")
184
+ ```
185
+
186
+ **Check 16 — Use Case diagrams present:**
187
+ ```
188
+ const ucDiagrams = diagrams.useCases || {}
189
+ const totalUCs = Object.values(FEATURE_DATA.moduleSpecs).reduce((sum, s) => sum + (s.useCases?.length || 0), 0)
190
+ IF Object.keys(ucDiagrams).length === 0 AND totalUCs > 0:
191
+ WARNING("No use case diagrams generated — UC tabs will have no UML visual")
192
+ ```
193
+
194
+ > **Checks 10-16 produce WARNINGs (non-blocking).** Display all warnings in the completion summary so the user is aware of potential quality issues.
109
195
 
110
196
  ---
111
197
 
112
- ### 2. Display Completion Summary
198
+ ### 3. Display Completion Summary
113
199
 
114
200
  ```
115
201
  ══════════════════════════════════════════════════════════════
@@ -131,5 +217,8 @@ Modules: {count} ({names})
131
217
  4. If approved: /business-analyse-handoff then /business-analyse-develop to begin development
132
218
  5. If corrections needed: export ba-review.json from the HTML, then run /business-analyse-review
133
219
 
220
+ Quality Warnings: {warning_count}
221
+ {FOR each warning: display warning message}
222
+
134
223
  ══════════════════════════════════════════════════════════════
135
224
  ```