@atlashub/smartstack-cli 3.26.0 → 3.28.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": "3.26.0",
3
+ "version": "3.28.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -112,7 +112,5 @@
112
112
  "typescript": "^5.3.3",
113
113
  "vitest": "^2.1.0"
114
114
  },
115
- "optionalDependencies": {
116
- "msnodesqlv8": "^5.1.3"
117
- }
115
+ "optionalDependencies": {}
118
116
  }
@@ -491,7 +491,7 @@ const seedDataCore = {
491
491
  ),
492
492
 
493
493
  navigationTranslations: master.modules.flatMap(m => {
494
- const langs = ["fr", "en"]; // minimum required
494
+ const langs = ["fr", "en", "it", "de"]; // all supported languages
495
495
  return langs.map(lang => ({
496
496
  moduleCode: m.code,
497
497
  language: lang,
@@ -450,7 +450,7 @@ Handoff Completeness:
450
450
  For EACH module feature.json:
451
451
 
452
452
  1. Extract all i18n keys from specification.i18nKeys[]
453
- 2. Verify each key has translations for: fr, en, nl, de
453
+ 2. Verify each key has translations for: fr, en, it, de
454
454
  3. Check for common missing patterns:
455
455
  ```
456
456
  - Entity names (singular + plural)
@@ -467,7 +467,7 @@ For EACH module feature.json:
467
467
  i18n Keys Validation:
468
468
  Module: Projects
469
469
  ✓ 42 keys defined
470
- ✓ All keys have 4 languages (fr, en, nl, de)
470
+ ✓ All keys have 4 languages (fr, en, it, de)
471
471
  ✓ Entity keys complete
472
472
  ✓ Validation message keys complete
473
473
  ✓ Navigation keys complete
@@ -99,6 +99,18 @@ Execution sequence:
99
99
  - Missing translations (must have all 4 languages)
100
100
  - Seeding business entities WITHOUT `TenantId`
101
101
 
102
+ ### POST-CHECK: Navigation translations diacritical marks
103
+
104
+ After generating `NavigationTranslationSeedEntry` strings, verify ALL non-English translations contain proper diacritical marks:
105
+
106
+ - **FR** must use: é, è, ê, ë, à, â, ç, ù, û, ô, î (e.g., "Employés" NOT "Employes", "Activités" NOT "Activites")
107
+ - **DE** must use: ä, ö, ü, ß (e.g., "Aktivitäten" NOT "Aktivitaten")
108
+ - **IT** must use: à, è, é, ì, ò, ù (e.g., "Attività" NOT "Attivita")
109
+
110
+ Cross-reference navigation seed data labels with the corresponding frontend `i18n/index.ts` translations for consistency.
111
+
112
+ **BLOCKING** if any `NavigationTranslationSeedEntry` for fr/de/it contains only ASCII characters where diacritical marks are expected in the target language.
113
+
102
114
  ---
103
115
 
104
116
  ## Infrastructure — SQL Objects
@@ -305,12 +317,13 @@ fi
305
317
  **FORBIDDEN:** `src/pages/{Module}/` (flat structure without Context/App)
306
318
 
307
319
  **Execution sequence (IN ORDER):**
320
+ 0. **Read back feature.json** (via `prd.source.featurePath`) — The PRD only carries the file list (structural skeleton). The behavioral specs (columnDefs, componentMapping, layout, rowActions, emptyState, tab structure, i18n key-value pairs, dashboard KPIs, state machine transitions) are ONLY in the original feature.json. Without this step, generated code will be generic and miss entity-specific UI details.
308
321
  1. `mcp__smartstack__scaffold_api_client` → API client + types + React Query hook
309
322
  2. `mcp__smartstack__scaffold_routes` (with `outputFormat: "clientRoutes"`) → route registry + route fragments
310
323
  3. **Wire routes to App.tsx (detect pattern: `contextRoutes` array OR JSX `<Route>`):**
311
324
  - **Pattern A** (`contextRoutes: ContextRouteExtensions` in App.tsx): add to `contextRoutes.{context}[]` with RELATIVE paths → auto-injected into both standard + tenant trees
312
325
  - **Pattern B** (JSX `<Route>` in App.tsx): insert `<Route>` entries inside correct Layout wrapper (BOTH standard AND tenant-prefixed `/t/:slug/...` blocks)
313
- 4. Create pages using **`/ui-components` skill** (MANDATORY for entity lists, grids, tables, dashboards, charts). The skill ensures CSS variables, EntityCard, SmartTable, loading/error/empty states, and responsive grid patterns.
326
+ 4. Create pages using **`/ui-components` skill** — **LOAD the skill step files before creating any page.** This is MANDATORY for entity lists, grids, tables, dashboards, charts. The skill ensures CSS variables, EntityCard, SmartTable, loading/error/empty states, and responsive grid patterns. **Pages MUST use `t()` from `useTranslation()` for ALL visible text from the very first line of code.** Do NOT hardcode English strings with the intent of adding i18n later — the i18n keys are created in step 6 based on what keys the pages reference. Pattern: `t('{moduleLower}:actions.create', 'Create')`.
314
327
  5. Create preferences hook: `use{Module}Preferences.ts`
315
328
  6. Generate i18n (4 languages: fr, en, it, de) — see I18n section below for detailed rules
316
329
  7. `npm run typecheck` MUST pass (BLOCKING)
@@ -323,6 +336,18 @@ fi
323
336
  - FK fields: `EntityLookup` for searchable entity selection (NEVER plain text for Guid FK)
324
337
  - Dashboard: `StatCard`, Recharts components
325
338
 
339
+ **Per-page /ui-components validation (MANDATORY before commit):**
340
+ - After creating EACH page, verify it follows /ui-components patterns:
341
+ 1. List pages: uses `DataTable` or `SmartTable` (NOT raw `<table>`)
342
+ 2. List pages: uses `EntityCard` for card/grid views (NOT custom `<div>` cards)
343
+ 3. All pages: ALL colors use CSS variables (`bg-[var(--xxx)]`) — ZERO hardcoded Tailwind colors
344
+ 4. All pages: loading skeleton, error state with retry, empty state are present
345
+ 5. All pages: ALL visible text uses `t()` from `useTranslation()`
346
+ 6. Delete actions: use `ConfirmDialog` component (NOT `window.confirm()`)
347
+ 7. Status displays: use `StatusBadge` with CSS variable colors (NOT inline styled spans)
348
+ - **If ANY check fails: fix the page BEFORE moving to the next page**
349
+ - The POST-CHECKs in step-02 enforce these rules with BLOCKING severity
350
+
326
351
  **Form pages (CRITICAL — ZERO modals/popups/drawers):**
327
352
  - Create form: `EntityCreatePage.tsx` with route `/{module}/create`
328
353
  - Edit form: `EntityEditPage.tsx` with route `/{module}/:id/edit`
@@ -340,6 +365,15 @@ fi
340
365
  | `business.*` | `BusinessLayout` | `/business` |
341
366
  | `personal.*` | `UserLayout` | `/personal/myspace` |
342
367
 
368
+ **Route naming convention (CRITICAL — must match backend):**
369
+ - Frontend route paths MUST use **kebab-case** matching the backend API route convention
370
+ - Convention: `HumanResources` (C# namespace) → `human-resources` (URL segment)
371
+ - ALL multi-word route segments MUST contain hyphens: `time-management`, `human-resources`, `leave-balance`
372
+ - FORBIDDEN: `humanresources`, `timemanagement`, `leavebalance` (concatenated without hyphens)
373
+ - Frontend path `/business/human-resources/clients` matches API `api/business/human-resources/clients`
374
+ - Navigation seed data Route values MUST also use kebab-case
375
+ - The POST-CHECK in step-02 will BLOCK if frontend routes don't match backend kebab-case convention
376
+
343
377
  **CSS:** Variables ONLY → `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`
344
378
 
345
379
  **Form error handling (MANDATORY):**
@@ -348,6 +382,13 @@ fi
348
382
  - API errors (4xx, 5xx) MUST show a user-friendly error notification (toast/banner)
349
383
  - Forms MUST preserve user input on error (no data loss on failed submit)
350
384
 
385
+ **Hook error handling pattern (MANDATORY):**
386
+ - ALL hooks MUST preserve server validation errors from axios responses
387
+ - Pattern: `catch (err) { if (axios.isAxiosError(err)) { return err.response?.data; } throw err; }`
388
+ - NEVER discard `err.response.data` — it contains field-level validation errors from FluentValidation
389
+ - Error messages MUST use i18n: `setError(t('{mod}:errors.saveFailed'))` — NEVER hardcoded English strings
390
+ - Toast/notification messages MUST also use i18n
391
+
351
392
  **Dependency verification (BLOCKING):**
352
393
  - BEFORE writing any import, verify the package exists in `package.json`
353
394
  - If package not present: run `npm install {package}` BEFORE writing the import
@@ -372,6 +413,13 @@ useXxx with raw axios inside hooks → hooks MUST use the shared apiCl
372
413
  placeholder="Enter ID" / "Enter GUID" → EntityLookup provides search-based entity selection
373
414
  <Modal>/<Dialog>/<Drawer>/<Popup> for forms → forms are FULL PAGES with own URL routes (/create, /:id/edit)
374
415
  useState(showCreateModal/editDialog) → navigate to form page, NEVER toggle modal visibility
416
+ <table>/<thead>/<tbody> → MUST use SmartTable or DataTable component
417
+ bg-blue-600 / bg-red-500 / etc. → MUST use CSS variables: bg-[var(--color-accent-600)]
418
+ text-green-700 / text-gray-500 / etc. → MUST use CSS variables: text-[var(--success-text)]
419
+ window.confirm() / confirm() → MUST use ConfirmDialog component with i18n
420
+ Hardcoded English text in JSX (>Create<) → MUST use t('{mod}:actions.create', 'Create')
421
+ Hardcoded error strings in hooks → MUST use t('{mod}:errors.loadFailed') in all hooks
422
+ /business/humanresources/ → MUST use kebab-case: /business/human-resources/
375
423
  ```
376
424
 
377
425
  ---
@@ -399,11 +447,14 @@ useState(showCreateModal/editDialog) → navigate to form page, NEVER to
399
447
  **Usage pattern in TSX:** `t('{moduleLower}:actions.create', 'Create')` — ALWAYS namespace prefix + fallback value.
400
448
 
401
449
  **Rules:**
402
- - 4 JSON files: fr, en, it, de — ALL mandatory (not just fr/en)
450
+ - 4 JSON files per module: fr, en, it, de — ALL mandatory (not just fr/en)
451
+ - File structure: `src/i18n/locales/{lang}/{moduleLower}.json` — NEVER inline translations in index.ts
403
452
  - Identical key structures across all languages — same keys, translated values
404
- - All UI labels, validation messages, button text, empty states
453
+ - All UI labels, validation messages, button text, empty states, error messages
405
454
  - Depends on frontend page completion (pages define which keys are needed)
406
455
  - Add entity-specific keys under `labels`, `columns`, `form` for each entity field
456
+ - **Hooks MUST also use i18n** — error messages like "Failed to load" MUST use `t('{mod}:errors.loadFailed')`
457
+ - **FORBIDDEN:** Inline translation objects in `i18n/index.ts` — use separate JSON files per language per module
407
458
  - Reference `smartstack-frontend.md` for the complete template
408
459
 
409
460
  ---
@@ -69,6 +69,7 @@ Batch: {batch.length} [{firstCategory}] → {batch.map(t => `[${t.id}] ${t.descr
69
69
 
70
70
  1. Mark `task.status = 'in_progress'`, `task.started_at = now`
71
71
  2. **Read `references/category-rules.md`** for category-specific rules
72
+ 2bis. **For frontend tasks:** Also read back the original **feature.json** (via `prd.source.featurePath`) to access wireframe `componentMapping`, `columnDefs`, `layout`, `emptyState`, `rowActions`, and `i18n` keys that the PRD does NOT carry forward. The PRD only has the file list — the behavioral spec is in feature.json.
72
73
  3. Implement the task following SmartStack conventions
73
74
  4. Track `files_created` and `files_modified`
74
75
 
@@ -110,7 +111,7 @@ Batch: {batch.length} [{firstCategory}] → {batch.map(t => `[${t.id}] ${t.descr
110
111
  |----------|--------|
111
112
  | `infrastructure` with `_migrationMeta` | Migration sequence: `suggest_migration` MCP → `dotnet ef migrations add` → cleanup corrupted artifacts (`for d in src/*/bin?Debug; do [ -d "$d" ] && rm -rf "$d"; done`) → `dotnet ef database update` → `dotnet build` |
112
113
  | `infrastructure` with seed data keywords | **MANDATORY:** Read `references/core-seed-data.md` → implement templates → `dotnet build` |
113
- | `frontend` | MCP-first: `scaffold_api_client` → `scaffold_routes` (outputFormat: clientRoutes) → **wire to App.tsx (detect pattern: `contextRoutes` array OR JSX `<Route>`)** → create pages → `npm run typecheck && npm run lint` |
114
+ | `frontend` | MCP-first: `scaffold_api_client` → `scaffold_routes` (outputFormat: clientRoutes) → **wire to App.tsx** **LOAD `/ui-components` skill** before creating ANY page → create pages using skill patterns (SmartTable, EntityCard, CSS variables, StatusBadge) ALL visible text via `t()` from first line → `npm run typecheck && npm run lint` → **run frontend POST-CHECKs (section 3bis below)** |
114
115
  | `test` | **Create tests:** `scaffold_tests` MCP → **Run:** `dotnet test --verbosity normal` → **Fix loop:** if fail → fix SOURCE CODE → rebuild → retest → repeat until 100% pass |
115
116
  | `validation` | `dotnet clean && dotnet restore && dotnet build` → `dotnet test` (full) → `validate_conventions` MCP |
116
117
 
@@ -161,7 +162,112 @@ Batch: {batch.length} [{firstCategory}] → {batch.map(t => `[${t.id}] ${t.descr
161
162
  ```
162
163
  If FAIL → fix → re-check → loop until pass
163
164
 
164
- 3bis. **Dependency audit (if frontend tasks in batch):**
165
+ 3bis. **Frontend POST-CHECKs (BLOCKING — if frontend tasks in batch):**
166
+
167
+ > **CRITICAL:** These checks enforce /ui-components compliance, i18n, and route conventions.
168
+ > Without them, generated code silently regresses to raw HTML tables, hardcoded colors, and English-only text.
169
+
170
+ ```bash
171
+ # Resolve paths dynamically (handles web/ prefix)
172
+ PAGE_DIR=$(find . -path "*/src/pages" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
173
+ HOOK_DIR=$(find . -path "*/src/hooks" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
174
+ COMP_DIR=$(find . -path "*/src/components" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
175
+ WEB_SRC=$(find . -name "App.tsx" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
176
+
177
+ if [ -n "$PAGE_DIR" ]; then
178
+ # BLOCKING: No raw HTML <table> — MUST use SmartTable/DataTable
179
+ RAW_TABLES=$(grep -rPn '<table[\s>]|<thead[\s>]|<tbody[\s>]' "$PAGE_DIR" --include="*.tsx" 2>/dev/null)
180
+ if [ -n "$RAW_TABLES" ]; then
181
+ echo "BLOCKING: Raw HTML <table> detected — MUST use SmartTable or DataTable"
182
+ echo "$RAW_TABLES" | head -5
183
+ # FIX REQUIRED before commit
184
+ fi
185
+
186
+ # BLOCKING: No hardcoded Tailwind colors — MUST use CSS variables
187
+ HARDCODED_COLORS=$(grep -rPn '(?:bg|text|border|ring)-(?:red|blue|green|yellow|orange|purple|pink|indigo|teal|cyan|emerald|violet|fuchsia|rose|amber|lime|sky|slate|gray|zinc|neutral|stone)-\d{2,3}' "$PAGE_DIR" "$COMP_DIR" --include="*.tsx" 2>/dev/null | head -10)
188
+ if [ -n "$HARDCODED_COLORS" ]; then
189
+ echo "BLOCKING: Hardcoded Tailwind colors — MUST use CSS variables (bg-[var(--bg-secondary)])"
190
+ echo "$HARDCODED_COLORS" | head -5
191
+ # FIX REQUIRED before commit
192
+ fi
193
+
194
+ # BLOCKING: No window.confirm() — use ConfirmDialog component
195
+ BAD_CONFIRM=$(grep -rPn 'window\.confirm\s*\(' "$PAGE_DIR" --include="*.tsx" 2>/dev/null | grep -v '//')
196
+ if [ -n "$BAD_CONFIRM" ]; then
197
+ echo "BLOCKING: window.confirm() detected — use ConfirmDialog component with i18n"
198
+ echo "$BAD_CONFIRM"
199
+ # FIX REQUIRED before commit
200
+ fi
201
+
202
+ # BLOCKING: No hardcoded English UI text in JSX
203
+ HARDCODED_TEXT=$(grep -rPn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No .* found|Are you sure|Submit|Back|Actions|Status)\s*<' "$PAGE_DIR" --include="*.tsx" 2>/dev/null | grep -v test | grep -v '//' | head -10)
204
+ if [ -n "$HARDCODED_TEXT" ]; then
205
+ echo "BLOCKING: Hardcoded English text — ALL visible text MUST use t() from useTranslation()"
206
+ echo "$HARDCODED_TEXT" | head -5
207
+ # FIX REQUIRED before commit
208
+ fi
209
+
210
+ # BLOCKING: No modals/drawers for forms — forms are full pages
211
+ MODAL_IMPORTS=$(grep -rPn "import.*\b(Modal|Dialog|Drawer|Popup|Sheet)\b" "$PAGE_DIR" --include="*.tsx" 2>/dev/null)
212
+ if [ -n "$MODAL_IMPORTS" ]; then
213
+ echo "BLOCKING: Form modals/dialogs detected — forms MUST be full pages with own URL"
214
+ echo "$MODAL_IMPORTS"
215
+ # FIX REQUIRED before commit
216
+ fi
217
+ fi
218
+
219
+ if [ -n "$HOOK_DIR" ]; then
220
+ # BLOCKING: Hooks must use i18n for error messages
221
+ HARDCODED_ERRORS=$(grep -rPn "(setError|throw new Error)\(['\"][A-Z]" "$HOOK_DIR" --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "t(" | head -10)
222
+ if [ -n "$HARDCODED_ERRORS" ]; then
223
+ echo "BLOCKING: Hardcoded error messages in hooks — MUST use t('{mod}:errors.xxx')"
224
+ echo "$HARDCODED_ERRORS" | head -5
225
+ # FIX REQUIRED before commit
226
+ fi
227
+ fi
228
+
229
+ # BLOCKING: i18n file structure — 4 languages with module-specific JSON files
230
+ if [ -n "$WEB_SRC" ]; then
231
+ I18N_DIR="$WEB_SRC/i18n/locales"
232
+ if [ -d "$I18N_DIR" ]; then
233
+ for LANG in fr en it de; do
234
+ if [ ! -d "$I18N_DIR/$LANG" ]; then
235
+ echo "BLOCKING: Missing i18n locale directory: $I18N_DIR/$LANG"
236
+ # FIX REQUIRED before commit
237
+ fi
238
+ MODULE_JSONS=$(find "$I18N_DIR/$LANG" -name "*.json" ! -name "common.json" ! -name "navigation.json" 2>/dev/null)
239
+ if [ -z "$MODULE_JSONS" ]; then
240
+ echo "BLOCKING: No module translation files in $I18N_DIR/$LANG/"
241
+ # FIX REQUIRED before commit
242
+ fi
243
+ done
244
+ else
245
+ echo "BLOCKING: Missing i18n/locales directory"
246
+ # FIX REQUIRED before commit
247
+ fi
248
+ fi
249
+
250
+ # Cross-validate route conventions (frontend vs backend)
251
+ APP_TSX=$(find . -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
252
+ API_CONTROLLERS=$(find . -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
253
+ if [ -n "$APP_TSX" ] && [ -n "$API_CONTROLLERS" ]; then
254
+ API_SEGMENTS=$(grep -ohP '\[Route\("api/([^"]+)"\)\]' $API_CONTROLLERS | sed 's/\[Route("api\///;s/")\]//' | tr '/' '\n' | sort -u)
255
+ CLIENT_PATHS=$(grep -ohP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | sed "s/path:\s*['\"]//;s/['\"]//" | sort -u)
256
+ for SEG in $API_SEGMENTS; do
257
+ if echo "$SEG" | grep -qP '[a-z]+-[a-z]+'; then
258
+ NO_HYPHEN=$(echo "$SEG" | tr -d '-')
259
+ if echo "$CLIENT_PATHS" | grep -q "$NO_HYPHEN"; then
260
+ echo "BLOCKING: Frontend uses '$NO_HYPHEN' but backend uses '$SEG' (kebab-case mismatch)"
261
+ # FIX REQUIRED before commit
262
+ fi
263
+ fi
264
+ done
265
+ fi
266
+ ```
267
+ If ANY POST-CHECK fails → fix the code → re-run ALL checks → loop until ALL pass.
268
+ **NEVER commit with failing POST-CHECKs.**
269
+
270
+ 3ter. **Dependency audit (if frontend tasks in batch):**
165
271
  ```bash
166
272
  cd {frontend_dir}
167
273
  npm ls --depth=0 2>&1 | grep "MISSING" && echo "FAIL: missing deps" && exit 1
@@ -99,6 +99,20 @@ if [ -n "$UPPERCASE_ROUTES" ]; then
99
99
  echo "Fix: Use ToKebabCase() helper in route generation (see core-seed-data.md)"
100
100
  exit 1
101
101
  fi
102
+
103
+ # Search for multi-word route segments missing hyphens (e.g., "humanresources" instead of "human-resources")
104
+ MULTI_WORD_ROUTES=$(grep -oP 'Route\s*=\s*"([^"]+)"' Infrastructure/Persistence/Seeding/Data/*/NavigationSeedData.cs 2>/dev/null | grep -P '"[^"]*(?:human(?:resources|resource)|time(?:management|tracking)|project(?:management|member)|leave(?:balance|request))[^"]*"')
105
+
106
+ if [ -n "$MULTI_WORD_ROUTES" ]; then
107
+ echo "❌ BLOCKING: Route segments contain concatenated multi-word names without hyphens"
108
+ echo "Found: $MULTI_WORD_ROUTES"
109
+ echo ""
110
+ echo "Convention: multi-word segments MUST use kebab-case"
111
+ echo " humanresources → human-resources"
112
+ echo " timemanagement → time-management"
113
+ echo "Fix: Apply ToKebabCase() to all route segments in NavigationSeedData"
114
+ exit 1
115
+ fi
102
116
  ```
103
117
 
104
118
  **Why this matters:**
@@ -244,9 +258,22 @@ npm run lint # ESLint — MUST exit 0
244
258
  If FAIL → read errors → fix TSX/TS files → re-run → loop until pass.
245
259
 
246
260
  **Frontend POST-CHECKs (BLOCKING for frontend category):**
261
+
262
+ > **Path resolution:** Web projects may live in subdirectories (e.g., `web/app-web/src/pages/`).
263
+ > All POST-CHECKs below use dynamic path discovery instead of hardcoded `src/pages/`.
264
+
265
+ ```bash
266
+ # Resolve frontend directories dynamically (handles web/ prefix and varied project structures)
267
+ PAGE_DIR=$(find . -path "*/src/pages" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
268
+ HOOK_DIR=$(find . -path "*/src/hooks" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
269
+ COMP_DIR=$(find . -path "*/src/components" -not -path "*/node_modules/*" -type d 2>/dev/null | head -1)
270
+ WEB_SRC=$(find . -name "App.tsx" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
271
+ # All POST-CHECKs below use $PAGE_DIR, $HOOK_DIR, $COMP_DIR, $WEB_SRC instead of hardcoded paths
272
+ ```
273
+
247
274
  ```bash
248
275
  # POST-CHECK: Forms must be full pages — ZERO modals/popups/drawers
249
- PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
276
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" 2>/dev/null)
250
277
  if [ -n "$PAGE_FILES" ]; then
251
278
  MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet)" $PAGE_FILES 2>/dev/null)
252
279
  if [ -n "$MODAL_IMPORTS" ]; then
@@ -257,8 +284,92 @@ if [ -n "$PAGE_FILES" ]; then
257
284
  fi
258
285
  fi
259
286
 
287
+ # POST-CHECK: No raw HTML <table> in page files — MUST use SmartTable/DataTable (BLOCKING)
288
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" 2>/dev/null)
289
+ if [ -n "$PAGE_FILES" ]; then
290
+ RAW_TABLES=$(grep -Pn '<table[\s>]|<thead[\s>]|<tbody[\s>]' $PAGE_FILES 2>/dev/null)
291
+ if [ -n "$RAW_TABLES" ]; then
292
+ echo "BLOCKING: Raw HTML <table> detected in page files — MUST use SmartTable or DataTable"
293
+ echo "SmartStack convention: Lists MUST use SmartTable + SmartFilter (see /ui-components skill)"
294
+ echo "Import: import { DataTable } from '@/components/ui/DataTable'"
295
+ echo ""
296
+ echo "Found:"
297
+ echo "$RAW_TABLES"
298
+ exit 1
299
+ fi
300
+ fi
301
+
302
+ # POST-CHECK: No hardcoded Tailwind colors — MUST use CSS variables (BLOCKING)
303
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" -o -name "*.tsx" 2>/dev/null | sort -u)
304
+ COMPONENT_FILES=$(find "$COMP_DIR" -name "*.tsx" 2>/dev/null)
305
+ ALL_TSX="$PAGE_FILES $COMPONENT_FILES"
306
+ if [ -n "$ALL_TSX" ]; then
307
+ # Match bg-{color}-{shade} or text-{color}-{shade} patterns (excluding bg-white, bg-black, bg-transparent, bg-current)
308
+ HARDCODED_COLORS=$(grep -Pn '(?:bg|text|border|ring|from|to|via)-(?:red|blue|green|yellow|orange|purple|pink|indigo|teal|cyan|emerald|violet|fuchsia|rose|amber|lime|sky|slate|gray|zinc|neutral|stone)-\d{2,3}' $ALL_TSX 2>/dev/null | head -20)
309
+ if [ -n "$HARDCODED_COLORS" ]; then
310
+ echo "BLOCKING: Hardcoded Tailwind colors detected — MUST use CSS variables"
311
+ echo "SmartStack convention: ALL colors via CSS variables for theming support"
312
+ echo " bg-blue-600 → bg-[var(--color-accent-600)]"
313
+ echo " text-red-500 → text-[var(--error-text)]"
314
+ echo " bg-green-100 → bg-[var(--success-bg)]"
315
+ echo " bg-gray-50 → bg-[var(--bg-secondary)]"
316
+ echo ""
317
+ echo "Found:"
318
+ echo "$HARDCODED_COLORS"
319
+ exit 1
320
+ fi
321
+ fi
322
+
323
+ # POST-CHECK: List pages must import SmartTable/DataTable OR EntityCard (WARNING)
324
+ LIST_PAGES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
325
+ if [ -n "$LIST_PAGES" ]; then
326
+ for LP in $LIST_PAGES; do
327
+ HAS_TABLE=$(grep -c 'SmartTable\|DataTable' "$LP" 2>/dev/null)
328
+ HAS_CARD=$(grep -c 'EntityCard' "$LP" 2>/dev/null)
329
+ if [ "$HAS_TABLE" -eq 0 ] && [ "$HAS_CARD" -eq 0 ]; then
330
+ echo "WARNING: List page missing SmartTable/DataTable or EntityCard: $LP"
331
+ echo "List pages MUST use SmartStack UI components from /ui-components skill"
332
+ echo " Tables: import { DataTable } from '@/components/ui/DataTable'"
333
+ echo " Cards: import { EntityCard } from '@/components/ui/EntityCard'"
334
+ fi
335
+ done
336
+ fi
337
+
338
+ # POST-CHECK: No window.confirm() — MUST use confirmation component (BLOCKING)
339
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" 2>/dev/null)
340
+ if [ -n "$PAGE_FILES" ]; then
341
+ BAD_CONFIRM=$(grep -Pn 'window\.confirm\s*\(|(?<!\w)confirm\s*\(' $PAGE_FILES 2>/dev/null | grep -v '//' | grep -v test)
342
+ if [ -n "$BAD_CONFIRM" ]; then
343
+ echo "BLOCKING: window.confirm() detected — MUST use a SmartStack confirmation component"
344
+ echo "Native browser dialogs break theming and i18n"
345
+ echo "Use a ConfirmDialog component with translated messages"
346
+ echo ""
347
+ echo "Found:"
348
+ echo "$BAD_CONFIRM"
349
+ exit 1
350
+ fi
351
+ fi
352
+
353
+ # POST-CHECK: Pages must handle loading, error, and empty states (WARNING)
354
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
355
+ if [ -n "$PAGE_FILES" ]; then
356
+ for LP in $PAGE_FILES; do
357
+ HAS_LOADING=$(grep -c 'loading\|isLoading\|skeleton\|Skeleton\|PageLoader\|spinner' "$LP" 2>/dev/null)
358
+ HAS_ERROR=$(grep -c 'error\|Error' "$LP" 2>/dev/null)
359
+ HAS_EMPTY=$(grep -c 'empty\|Empty\|no.*found\|length === 0' "$LP" 2>/dev/null)
360
+ MISSING=""
361
+ [ "$HAS_LOADING" -eq 0 ] && MISSING="${MISSING}loading "
362
+ [ "$HAS_ERROR" -eq 0 ] && MISSING="${MISSING}error "
363
+ [ "$HAS_EMPTY" -eq 0 ] && MISSING="${MISSING}empty "
364
+ if [ -n "$MISSING" ]; then
365
+ echo "WARNING: List page missing state handling ($MISSING): $LP"
366
+ echo "All list pages MUST handle: loading skeleton, error with retry, empty state"
367
+ fi
368
+ done
369
+ fi
370
+
260
371
  # POST-CHECK: Create/Edit pages must exist for each module with a list page
261
- LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
372
+ LIST_PAGES=$(find "$PAGE_DIR" -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
262
373
  if [ -n "$LIST_PAGES" ]; then
263
374
  for LP in $LIST_PAGES; do
264
375
  DIR=$(dirname "$LP"); MOD=$(basename "$DIR")
@@ -272,9 +383,9 @@ if [ -n "$LIST_PAGES" ]; then
272
383
  fi
273
384
 
274
385
  # First check: at least one test file must exist in pages/
275
- PAGE_FILES=$(find src/pages/ -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
386
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
276
387
  if [ -n "$PAGE_FILES" ]; then
277
- TEST_FILES=$(find src/pages/ -name "*.test.tsx" 2>/dev/null)
388
+ TEST_FILES=$(find "$PAGE_DIR" -name "*.test.tsx" 2>/dev/null)
278
389
  if [ -z "$TEST_FILES" ]; then
279
390
  echo "BLOCKING: No frontend test files found in src/pages/"
280
391
  echo "Every module with pages MUST have at least one .test.tsx file"
@@ -283,7 +394,7 @@ if [ -n "$PAGE_FILES" ]; then
283
394
  fi
284
395
 
285
396
  # POST-CHECK: Form pages must have companion test files
286
- FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
397
+ FORM_PAGES=$(find "$PAGE_DIR" -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
287
398
  if [ -n "$FORM_PAGES" ]; then
288
399
  for FP in $FORM_PAGES; do
289
400
  TEST="${FP%.tsx}.test.tsx"
@@ -295,7 +406,7 @@ if [ -n "$FORM_PAGES" ]; then
295
406
  fi
296
407
 
297
408
  # POST-CHECK: FK fields must NOT be plain text inputs — use EntityLookup
298
- FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
409
+ FORM_PAGES=$(find "$PAGE_DIR" -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
299
410
  if [ -n "$FORM_PAGES" ]; then
300
411
  # Match any input with name ending in "Id" (except hidden inputs)
301
412
  FK_TEXT_INPUTS=$(grep -Pn '<input[^>]*name=["\x27][a-zA-Z]*Id["\x27]' $FORM_PAGES 2>/dev/null | grep -v 'type=["\x27]hidden["\x27]')
@@ -326,15 +437,34 @@ if [ -n "$CTRL_FILES" ]; then
326
437
  done
327
438
  fi
328
439
 
329
- # POST-CHECK: Route seed data vs frontend cross-validation
440
+ # POST-CHECK: Route seed data vs frontend cross-validation (CONVENTION + EXISTENCE)
330
441
  SEED_NAV_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*NavigationSeedData.cs" 2>/dev/null)
331
- APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
442
+ APP_TSX="$WEB_SRC/App.tsx"
332
443
  if [ -n "$SEED_NAV_FILES" ] && [ -n "$APP_TSX" ]; then
333
444
  SEED_ROUTES=$(grep -ohP 'Route\s*=\s*"([^"]+)"' $SEED_NAV_FILES | sed 's/Route\s*=\s*"//' | sed 's/"//' | sort -u)
334
445
  CLIENT_PATHS=$(grep -ohP "path:\s*['\"]([^'\"]+)['\"]" "$APP_TSX" | sed "s/path:\s*['\"]//;s/['\"]//" | sort -u)
335
446
  if [ -n "$SEED_ROUTES" ] && [ -z "$CLIENT_PATHS" ]; then
336
447
  echo "WARNING: Seed data has navigation routes but App.tsx has no client routes defined"
337
448
  fi
449
+ # Cross-check: frontend paths must use same kebab-case segments as backend API routes
450
+ API_CONTROLLERS=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
451
+ if [ -n "$API_CONTROLLERS" ] && [ -n "$CLIENT_PATHS" ]; then
452
+ API_SEGMENTS=$(grep -ohP '\[Route\("api/([^"]+)"\)\]' $API_CONTROLLERS | sed 's/\[Route("api\///;s/")\]//' | tr '/' '\n' | sort -u)
453
+ for SEG in $API_SEGMENTS; do
454
+ # Check if frontend path contains same segment (kebab-case match)
455
+ if echo "$SEG" | grep -qP '[a-z]+-[a-z]+'; then
456
+ # Multi-word segment like "human-resources" — verify frontend uses same convention
457
+ NO_HYPHEN=$(echo "$SEG" | tr -d '-')
458
+ if echo "$CLIENT_PATHS" | grep -q "$NO_HYPHEN"; then
459
+ echo "BLOCKING: Frontend route uses '$NO_HYPHEN' but backend API uses '$SEG' (kebab-case)"
460
+ echo "Fix: Frontend paths MUST match backend route convention"
461
+ echo " Backend: api/business/$SEG/..."
462
+ echo " Frontend: /business/$SEG/... (NOT /business/$NO_HYPHEN/...)"
463
+ exit 1
464
+ fi
465
+ fi
466
+ done
467
+ fi
338
468
  fi
339
469
 
340
470
  # POST-CHECK: HasQueryFilter anti-pattern
@@ -359,20 +489,78 @@ if [ -n "$SERVICE_FILES" ]; then
359
489
  fi
360
490
  fi
361
491
 
362
- # POST-CHECK: i18n required key structure
363
- FR_I18N_FILES=$(find src/i18n/locales/fr/ -name "*.json" 2>/dev/null)
364
- if [ -n "$FR_I18N_FILES" ]; then
365
- for f in $FR_I18N_FILES; do
492
+ # POST-CHECK: i18n file structure — per-module JSON files MUST exist (BLOCKING)
493
+ WEB_DIR=$(find . -name "App.tsx" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
494
+ if [ -n "$WEB_DIR" ]; then
495
+ I18N_DIR="$WEB_DIR/i18n/locales"
496
+ if [ -d "$I18N_DIR" ]; then
497
+ for LANG in fr en it de; do
498
+ LANG_DIR="$I18N_DIR/$LANG"
499
+ if [ ! -d "$LANG_DIR" ]; then
500
+ echo "BLOCKING: Missing i18n locale directory: $LANG_DIR"
501
+ echo "All 4 languages (fr, en, it, de) MUST have a dedicated directory"
502
+ exit 1
503
+ fi
504
+ # Check for at least one module-specific JSON (not just common.json)
505
+ MODULE_JSONS=$(find "$LANG_DIR" -name "*.json" ! -name "common.json" ! -name "navigation.json" 2>/dev/null)
506
+ if [ -z "$MODULE_JSONS" ]; then
507
+ echo "BLOCKING: No module translation files in $LANG_DIR/"
508
+ echo "Each module MUST have src/i18n/locales/$LANG/{moduleLower}.json"
509
+ exit 1
510
+ fi
511
+ done
512
+ else
513
+ echo "BLOCKING: Missing i18n/locales directory — i18n file structure not created"
514
+ echo "Expected: src/i18n/locales/{fr,en,it,de}/{module}.json"
515
+ exit 1
516
+ fi
517
+ fi
518
+
519
+ # POST-CHECK: i18n required key structure (BLOCKING — upgraded from WARNING)
520
+ if [ -d "$I18N_DIR/fr" ]; then
521
+ for f in $(find "$I18N_DIR/fr/" -name "*.json" 2>/dev/null); do
366
522
  BASENAME=$(basename "$f")
367
523
  case "$BASENAME" in common.json|navigation.json) continue;; esac
368
- for KEY in "actions" "labels" "errors"; do
524
+ for KEY in "actions" "labels" "errors" "messages" "empty"; do
369
525
  if ! grep -q "\"$KEY\"" "$f"; then
370
- echo "WARNING: i18n file missing required key '$KEY': $f"
526
+ echo "BLOCKING: i18n file missing required key group '$KEY': $f"
527
+ echo "All module i18n files MUST contain: actions, labels, errors, messages, empty"
528
+ exit 1
371
529
  fi
372
530
  done
373
531
  done
374
532
  fi
375
533
 
534
+ # POST-CHECK: Pages must NOT contain hardcoded English UI text (BLOCKING)
535
+ PAGE_FILES=$(find "$PAGE_DIR" -name "*.tsx" ! -name "*.test.tsx" 2>/dev/null)
536
+ if [ -n "$PAGE_FILES" ]; then
537
+ # Detect common hardcoded English patterns in JSX (excluding imports/comments/code)
538
+ HARDCODED_TEXT=$(grep -Pn '>\s*(Create|Edit|Delete|Save|Cancel|Search|Loading|Error|No .* found|Are you sure|Submit|Back|Actions|Status|Name|Description|Total)\s*<' $PAGE_FILES 2>/dev/null | grep -v '//' | grep -v 'test' | head -20)
539
+ if [ -n "$HARDCODED_TEXT" ]; then
540
+ echo "BLOCKING: Hardcoded English UI text found in page files"
541
+ echo "ALL visible text MUST use t() from useTranslation() — NEVER hardcode strings"
542
+ echo "Pattern: t('{module}:actions.create', 'Create') — namespace + fallback"
543
+ echo ""
544
+ echo "Found:"
545
+ echo "$HARDCODED_TEXT"
546
+ exit 1
547
+ fi
548
+ fi
549
+
550
+ # POST-CHECK: Hooks must NOT contain hardcoded error messages (BLOCKING)
551
+ HOOK_FILES=$(find "$HOOK_DIR" -name "*.ts" -o -name "*.tsx" 2>/dev/null)
552
+ if [ -n "$HOOK_FILES" ]; then
553
+ HARDCODED_ERRORS=$(grep -Pn "(setError|throw new Error)\(['\"](?!t\()[A-Z][a-z]+" $HOOK_FILES 2>/dev/null | head -15)
554
+ if [ -n "$HARDCODED_ERRORS" ]; then
555
+ echo "BLOCKING: Hardcoded error messages in hooks — MUST use i18n t() function"
556
+ echo "Hooks must import useTranslation and use t('{module}:errors.loadFailed') for all messages"
557
+ echo ""
558
+ echo "Found:"
559
+ echo "$HARDCODED_ERRORS"
560
+ exit 1
561
+ fi
562
+ fi
563
+
376
564
  # POST-CHECK: SeedConstants must NOT contain ContextId (pre-seeded by SmartStack core)
377
565
  SEED_CONST_FILES=$(find src/ -path "*/Seeding/*" -name "SeedConstants.cs" 2>/dev/null)
378
566
  if [ -n "$SEED_CONST_FILES" ]; then
@@ -24,9 +24,9 @@ if [ "{current_task_category}" != "frontend" ] && [ "{current_task_category}" !=
24
24
  dotnet build --no-restore --verbosity quiet
25
25
  fi
26
26
 
27
- # Frontend: typecheck must pass
27
+ # Frontend: typecheck + lint must pass
28
28
  if [ "{current_task_category}" = "frontend" ]; then
29
- npm run typecheck
29
+ npm run typecheck && npm run lint
30
30
  fi
31
31
 
32
32
  # Tests: full suite if test project exists
@@ -80,7 +80,7 @@ fi
80
80
 
81
81
  ```javascript
82
82
  const presentCategories = new Set(prd.tasks.map(t => t.category));
83
- const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'frontend', 'test'];
83
+ const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
84
84
  const missingFromPrd = REQUIRED_CATEGORIES.filter(c => !presentCategories.has(c));
85
85
 
86
86
  if (missingFromPrd.length > 0) {
@@ -90,6 +90,7 @@ if (missingFromPrd.length > 0) {
90
90
  let maxIdNum = Math.max(...prd.tasks.map(t => parseInt(t.id.replace(/[^0-9]/g, ''), 10) || 0));
91
91
  const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'GUARD-';
92
92
  const lastApiTask = prd.tasks.filter(t => t.category === 'api').pop()?.id;
93
+ const lastSeedDataTask = prd.tasks.filter(t => t.category === 'seedData').pop()?.id;
93
94
 
94
95
  for (const cat of missingFromPrd) {
95
96
  maxIdNum++;
@@ -98,7 +99,7 @@ if (missingFromPrd.length > 0) {
98
99
  id: taskId,
99
100
  description: `[GUARDRAIL] Generate missing ${cat} layer for ${prd.project?.module || 'module'}`,
100
101
  status: 'pending', category: cat,
101
- dependencies: lastApiTask ? [lastApiTask] : [],
102
+ dependencies: lastSeedDataTask ? [lastSeedDataTask] : (lastApiTask ? [lastApiTask] : []),
102
103
  acceptance_criteria: [
103
104
  cat === 'frontend' ? 'React pages + routes wired to App.tsx (standard + tenant blocks)' :
104
105
  cat === 'test' ? 'Unit + integration test projects with passing dotnet test' :
@@ -179,7 +180,7 @@ if (fileExists(queuePath)) {
179
180
  // MODULE COMPLETENESS CHECK (lightweight — heavy check already done in section 1.7)
180
181
  // Verify all expected layers have completed tasks
181
182
  const completedCats = new Set(prd.tasks.filter(t => t.status === 'completed').map(t => t.category));
182
- const expected = ['domain', 'infrastructure', 'application', 'api', 'frontend', 'test'];
183
+ const expected = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
183
184
  const missing = expected.filter(c => !completedCats.has(c));
184
185
 
185
186
  if (missing.length > 0) {