@atlashub/smartstack-cli 3.21.0 → 3.22.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/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +68 -3
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/skills/application/references/application-roles-template.md +2 -2
- package/templates/skills/application/steps/step-05-frontend.md +40 -35
- package/templates/skills/application/templates-frontend.md +64 -36
- package/templates/skills/business-analyse/html/ba-interactive.html +80 -6
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +38 -6
- package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +42 -0
- package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -0
- package/templates/skills/business-analyse/references/deploy-data-build.md +5 -3
- package/templates/skills/business-analyse/references/handoff-file-templates.md +2 -1
- package/templates/skills/business-analyse/references/naming-conventions.md +245 -0
- package/templates/skills/business-analyse/references/validate-incremental-html.md +26 -4
- package/templates/skills/business-analyse/references/validation-checklist.md +31 -11
- package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -0
- package/templates/skills/business-analyse/steps/step-03b-ui.md +59 -0
- package/templates/skills/business-analyse/steps/step-03c-compile.md +114 -0
- package/templates/skills/business-analyse/steps/step-03d-validate.md +144 -22
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +114 -2
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +28 -0
- package/templates/skills/ralph-loop/references/category-rules.md +5 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +52 -1
- package/templates/skills/ralph-loop/references/core-seed-data.md +232 -21
- package/templates/skills/ralph-loop/steps/step-01-task.md +36 -4
- package/templates/skills/ralph-loop/steps/step-02-execute.md +81 -0
package/package.json
CHANGED
|
@@ -50,7 +50,7 @@ public static class ApplicationRolesSeedData
|
|
|
50
50
|
{
|
|
51
51
|
// Deterministic GUIDs for application roles
|
|
52
52
|
// Generated from: "role-{applicationId}-{roleType}"
|
|
53
|
-
|
|
53
|
+
public static readonly Guid ApplicationId = NavigationApplicationSeedData.ApplicationId;
|
|
54
54
|
|
|
55
55
|
public static readonly Guid AdminRoleId = GenerateRoleGuid("admin");
|
|
56
56
|
public static readonly Guid ManagerRoleId = GenerateRoleGuid("manager");
|
|
@@ -141,7 +141,7 @@ public class ApplicationRoleSeedEntry
|
|
|
141
141
|
|-------------|-------------|---------|
|
|
142
142
|
| `{BaseNamespace}` | Root namespace of the client project | `SmartStack.Modules.RessourcesHumaines` |
|
|
143
143
|
| `{AppLabel}` | Human-readable application label (EN) | `Human Resources` |
|
|
144
|
-
| `
|
|
144
|
+
| `ApplicationId` | Deterministic GUID from `NavigationApplicationSeedData.ApplicationId` — NO placeholder needed | Auto-resolved |
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
@@ -102,7 +102,7 @@ This generates:
|
|
|
102
102
|
|
|
103
103
|
> **CRITICAL:** This step is MANDATORY. Without it, routes exist as files but are invisible to the React Router. The page will be BLANK.
|
|
104
104
|
|
|
105
|
-
After `scaffold_routes` generates the route files, you MUST manually insert the routes into `App.tsx
|
|
105
|
+
After `scaffold_routes` generates the route files, you MUST manually insert the routes into `App.tsx`.
|
|
106
106
|
|
|
107
107
|
**Step 4a:** Import the page components at the top of App.tsx:
|
|
108
108
|
|
|
@@ -112,33 +112,49 @@ import { {EntityName}Page } from '@/pages/{Context}/{Application}/{Module}/{Enti
|
|
|
112
112
|
const {EntityName}Page = lazy(() => import('@/pages/{Context}/{Application}/{Module}/{EntityName}Page'));
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
**Step 4b:**
|
|
115
|
+
**Step 4b:** Detect the App.tsx routing pattern:
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|---------|---------------------|
|
|
119
|
-
| `platform.*` | `<Route path="/platform" element={<AdminLayout />}>` |
|
|
120
|
-
| `business.*` | `<Route path="/business" element={<BusinessLayout />}>` |
|
|
121
|
-
| `personal.*` | `<Route path="/personal/myspace" element={<UserLayout />}>` |
|
|
117
|
+
Read App.tsx and detect which pattern is used:
|
|
122
118
|
|
|
123
|
-
**
|
|
119
|
+
**Pattern A** — If App.tsx contains `contextRoutes: ContextRouteExtensions`:
|
|
120
|
+
→ Add routes to `contextRoutes.{context}[]` with **RELATIVE** paths (no leading `/`)
|
|
124
121
|
|
|
125
|
-
```tsx
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
```tsx
|
|
123
|
+
const contextRoutes: ContextRouteExtensions = {
|
|
124
|
+
business: [
|
|
125
|
+
// existing routes...
|
|
126
|
+
{ path: '{application}/{module}/list', element: <{EntityName}ListPage /> },
|
|
127
|
+
{ path: '{application}/{module}/new', element: <Create{EntityName}Page /> },
|
|
128
|
+
{ path: '{application}/{module}/:id', element: <{EntityName}DetailPage /> },
|
|
129
|
+
{ path: '{application}/{module}/:id/edit', element: <Create{EntityName}Page /> },
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
```
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
Routes are automatically injected into BOTH standard (`/business/...`) and tenant-prefixed (`/t/:slug/business/...`) route trees by `mergeRoutes()`. No manual duplication needed.
|
|
135
|
+
|
|
136
|
+
**Pattern B** — If App.tsx contains `<Route path="/{context}" element={<{Layout} />}>` (JSX Routes):
|
|
137
|
+
→ Insert `<Route>` children inside the Layout wrapper (see below)
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
```tsx
|
|
140
|
+
<Route path="/business" element={<BusinessLayout />}>
|
|
141
|
+
{/* ... existing routes ... */}
|
|
142
|
+
<Route path="{application}/{module}" element={<{EntityName}Page />} />
|
|
143
|
+
</Route>
|
|
144
|
+
```
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
ALSO add the same routes inside the tenant-prefixed block:
|
|
147
|
+
Find `<Route path="/t/:slug">` and add the **same route entries** there.
|
|
140
148
|
|
|
141
|
-
**Step
|
|
149
|
+
**Step 4c:** Context-to-Layout mapping:
|
|
150
|
+
|
|
151
|
+
| Context | Layout Component | Route path |
|
|
152
|
+
|---------|------------------|------------|
|
|
153
|
+
| `platform.*` | `AdminLayout` | `/platform` |
|
|
154
|
+
| `business.*` | `BusinessLayout` | `/business` |
|
|
155
|
+
| `personal.*` | `UserLayout` | `/personal/myspace` |
|
|
156
|
+
|
|
157
|
+
**Step 4d:** Verify wiring:
|
|
142
158
|
|
|
143
159
|
```
|
|
144
160
|
Tool: mcp__smartstack__validate_frontend_routes
|
|
@@ -148,21 +164,10 @@ Args:
|
|
|
148
164
|
|
|
149
165
|
If `appWiring.issues` is not empty, fix the wiring before proceeding.
|
|
150
166
|
|
|
151
|
-
**
|
|
152
|
-
|
|
153
|
-
If the context doesn't have a layout route yet, create one wrapping all child routes:
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
<Route path="/{context}/{application}" element={<{Context}Layout />}>
|
|
157
|
-
<Route index element={<Navigate to="{default_module}" replace />} />
|
|
158
|
-
<Route path="{module}" element={<{EntityName}Page />} />
|
|
159
|
-
</Route>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
**FORBIDDEN:**
|
|
167
|
+
**FORBIDDEN (both patterns):**
|
|
168
|
+
- Adding business/platform/personal routes to `clientRoutes[]` with absolute paths — `clientRoutes` is ONLY for routes outside SmartStack contexts (e.g., `/about`, `/pricing`)
|
|
163
169
|
- Adding routes OUTSIDE the Layout wrapper (shell will not render)
|
|
164
|
-
-
|
|
165
|
-
- Using `createBrowserRouter` (SmartStack uses `BrowserRouter` with JSX `<Routes>`)
|
|
170
|
+
- Using `createBrowserRouter` (SmartStack uses `useRoutes()` + `mergeRoutes()`)
|
|
166
171
|
|
|
167
172
|
### 5. Verify i18n Files
|
|
168
173
|
|
|
@@ -488,7 +488,51 @@ export const $moduleApi = {
|
|
|
488
488
|
|
|
489
489
|
## TEMPLATE: ROUTES (App.tsx)
|
|
490
490
|
|
|
491
|
-
###
|
|
491
|
+
### Detect App.tsx Routing Pattern FIRST
|
|
492
|
+
|
|
493
|
+
Before adding routes, **read App.tsx** and detect which pattern is used:
|
|
494
|
+
|
|
495
|
+
| Pattern | How to detect | Action |
|
|
496
|
+
|---------|---------------|--------|
|
|
497
|
+
| **Pattern A** (mergeRoutes) | `contextRoutes: ContextRouteExtensions` present | Add to `contextRoutes.{context}[]` array |
|
|
498
|
+
| **Pattern B** (JSX Routes) | `<Route path="/{context}" element={<{Layout} />}>` present | Insert `<Route>` children inside Layout wrapper |
|
|
499
|
+
|
|
500
|
+
### Pattern A: mergeRoutes (contextRoutes array)
|
|
501
|
+
|
|
502
|
+
> **This is the DEFAULT pattern** generated by `smartstack init`.
|
|
503
|
+
> Routes added to `contextRoutes` are automatically injected into BOTH standard and tenant-prefixed route trees by `mergeRoutes()`. No manual duplication needed.
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
// Add to App.tsx — imports at top
|
|
507
|
+
import { $MODULE_PASCALPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/$MODULE_PASCALPage';
|
|
508
|
+
import { $MODULE_PASCALDetailPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage';
|
|
509
|
+
import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/Create$MODULE_PASCALPage';
|
|
510
|
+
|
|
511
|
+
// Add routes to contextRoutes.{context}[] with RELATIVE paths (no leading /)
|
|
512
|
+
const contextRoutes: ContextRouteExtensions = {
|
|
513
|
+
$CONTEXT: [
|
|
514
|
+
// ... existing routes ...
|
|
515
|
+
{ path: '$APPLICATION/$MODULE', element: <$MODULE_PASCALPage /> },
|
|
516
|
+
{ path: '$APPLICATION/$MODULE/new', element: <Create$MODULE_PASCALPage /> },
|
|
517
|
+
{ path: '$APPLICATION/$MODULE/:id', element: <$MODULE_PASCALDetailPage /> },
|
|
518
|
+
{ path: '$APPLICATION/$MODULE/:id/edit', element: <Create$MODULE_PASCALPage /> },
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**mergeRoutes auto-generates redirects** for intermediate paths (e.g., `$APPLICATION` → `$APPLICATION/$DEFAULT_MODULE`) so you don't need to add index redirects manually.
|
|
524
|
+
|
|
525
|
+
**Context-to-Layout mapping (automatic via mergeRoutes):**
|
|
526
|
+
|
|
527
|
+
| Context key | Injected into Layout | Standard path | Tenant path |
|
|
528
|
+
|-------------|---------------------|---------------|-------------|
|
|
529
|
+
| `platform` | `AdminLayout` | `/platform/...` | `/t/:slug/platform/...` |
|
|
530
|
+
| `business` | `BusinessLayout` | `/business/...` | `/t/:slug/business/...` |
|
|
531
|
+
| `personal` | `UserLayout` | `/personal/myspace/...` | `/t/:slug/personal/myspace/...` |
|
|
532
|
+
|
|
533
|
+
### Pattern B: JSX Routes (inside Layout wrapper)
|
|
534
|
+
|
|
535
|
+
> **Legacy pattern** — only used if App.tsx was manually restructured with JSX `<Route>` elements.
|
|
492
536
|
|
|
493
537
|
SmartStack layouts (`AdminLayout`, `BusinessLayout`, `UserLayout`) provide the application shell: **header with AvatarMenu**, sidebar, navigation. They render child pages via React Router's `<Outlet />`.
|
|
494
538
|
|
|
@@ -504,13 +548,6 @@ SmartStack layouts (`AdminLayout`, `BusinessLayout`, `UserLayout`) provide the a
|
|
|
504
548
|
3. Add the new routes **INSIDE** that `<Route>` block
|
|
505
549
|
4. If a tenant-prefixed block exists (`/t/:slug/...`), add the routes there too
|
|
506
550
|
|
|
507
|
-
**❌ FORBIDDEN - Flat routes OUTSIDE layout (AvatarMenu disappears!)**
|
|
508
|
-
```tsx
|
|
509
|
-
// These routes bypass the layout system entirely = NO header, NO sidebar, NO AvatarMenu
|
|
510
|
-
<Route path="/$CONTEXT/$APPLICATION/$MODULE" element={<$MODULE_PASCALPage />} />
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
**✅ MANDATORY - Routes INSIDE the existing layout wrapper**
|
|
514
551
|
```tsx
|
|
515
552
|
// Add to App.tsx
|
|
516
553
|
|
|
@@ -533,42 +570,33 @@ import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/
|
|
|
533
570
|
</Route>
|
|
534
571
|
```
|
|
535
572
|
|
|
536
|
-
###
|
|
573
|
+
### ⚠️ CRITICAL RULES (both patterns)
|
|
537
574
|
|
|
538
|
-
|
|
539
|
-
|---------|------------------|-------------------|
|
|
540
|
-
| `platform` | `AdminLayout` | `/platform` |
|
|
541
|
-
| `business` | `BusinessLayout` | `/business` |
|
|
542
|
-
| `personal` | `UserLayout` | `/personal/myspace` |
|
|
543
|
-
|
|
544
|
-
### ⚠️ CRITICAL RULE 2: NESTED ROUTES MANDATORY
|
|
545
|
-
|
|
546
|
-
React Router v7 **requires** nested routes for multi-module applications.
|
|
547
|
-
|
|
548
|
-
**❌ FORBIDDEN - Flat routes (cause redirects to Home)**
|
|
575
|
+
**FORBIDDEN — Adding to clientRoutes[] with absolute paths:**
|
|
549
576
|
```tsx
|
|
550
|
-
|
|
551
|
-
|
|
577
|
+
// ❌ WRONG — bypasses layout entirely, shell will NOT render
|
|
578
|
+
const clientRoutes: RouteConfig[] = [
|
|
579
|
+
{ path: '/business/$APPLICATION/$MODULE', element: <$MODULE_PASCALPage /> },
|
|
580
|
+
];
|
|
552
581
|
```
|
|
553
582
|
|
|
554
|
-
|
|
583
|
+
`clientRoutes` is ONLY for routes **outside** SmartStack locked contexts (e.g., `/about`, `/pricing`).
|
|
584
|
+
|
|
585
|
+
**FORBIDDEN — Flat routes outside layout:**
|
|
555
586
|
```tsx
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
<Route path="$MODULE" element={<$MODULE_PASCALPage />} />
|
|
559
|
-
</Route>
|
|
587
|
+
// ❌ WRONG (Pattern B only) — flat route bypasses layout
|
|
588
|
+
<Route path="/$CONTEXT/$APPLICATION/$MODULE" element={<$MODULE_PASCALPage />} />
|
|
560
589
|
```
|
|
561
590
|
|
|
562
|
-
### Why
|
|
591
|
+
### Why nested/context routes?
|
|
563
592
|
|
|
564
|
-
| Aspect |
|
|
565
|
-
|
|
566
|
-
| Shell rendered |
|
|
567
|
-
| AvatarMenu visible |
|
|
568
|
-
|
|
|
569
|
-
|
|
|
570
|
-
|
|
|
571
|
-
| Redirect | Can fail | Always works |
|
|
593
|
+
| Aspect | clientRoutes (wrong) | contextRoutes / nested (correct) |
|
|
594
|
+
|--------|---------------------|----------------------------------|
|
|
595
|
+
| Shell rendered | No (bypasses layout) | Yes (Outlet pattern) |
|
|
596
|
+
| AvatarMenu visible | No | Yes |
|
|
597
|
+
| Tenant prefix | Manual duplication | Automatic (mergeRoutes) |
|
|
598
|
+
| Auto-redirects | None | Generated for intermediate paths |
|
|
599
|
+
| Permission check | Bypassed (no RouteGuard) | Enforced by RouteGuard |
|
|
572
600
|
|
|
573
601
|
---
|
|
574
602
|
|
|
@@ -993,6 +993,48 @@ body {
|
|
|
993
993
|
.svg-wireframe svg {
|
|
994
994
|
max-width: 100%;
|
|
995
995
|
height: auto;
|
|
996
|
+
display: block;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/* ============================================
|
|
1000
|
+
WIREFRAME VIEW TOGGLE (SVG / ASCII)
|
|
1001
|
+
============================================ */
|
|
1002
|
+
.wireframe-toggle {
|
|
1003
|
+
display: flex;
|
|
1004
|
+
gap: 2px;
|
|
1005
|
+
background: var(--bg-dark);
|
|
1006
|
+
border-radius: 6px;
|
|
1007
|
+
padding: 2px;
|
|
1008
|
+
border: 1px solid var(--border);
|
|
1009
|
+
margin-left: auto;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.wireframe-toggle-btn {
|
|
1013
|
+
padding: 0.25rem 0.6rem;
|
|
1014
|
+
font-size: 0.7rem;
|
|
1015
|
+
font-weight: 500;
|
|
1016
|
+
letter-spacing: 0.02em;
|
|
1017
|
+
border: none;
|
|
1018
|
+
border-radius: 4px;
|
|
1019
|
+
cursor: pointer;
|
|
1020
|
+
background: transparent;
|
|
1021
|
+
color: var(--text-muted);
|
|
1022
|
+
transition: all 0.15s ease;
|
|
1023
|
+
font-family: inherit;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.wireframe-toggle-btn:hover {
|
|
1027
|
+
color: var(--text-bright);
|
|
1028
|
+
background: var(--bg-hover);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
.wireframe-toggle-btn.active {
|
|
1032
|
+
background: var(--primary);
|
|
1033
|
+
color: #ffffff;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.wireframe-view:not(.active) {
|
|
1037
|
+
display: none;
|
|
996
1038
|
}
|
|
997
1039
|
|
|
998
1040
|
.wireframe-description {
|
|
@@ -3279,18 +3321,30 @@ function renderModuleMockups(code) {
|
|
|
3279
3321
|
</div>`;
|
|
3280
3322
|
}
|
|
3281
3323
|
|
|
3282
|
-
return wireframes.map((wf, i) =>
|
|
3324
|
+
return wireframes.map((wf, i) => {
|
|
3325
|
+
const hasSvg = !!wf.svgContent;
|
|
3326
|
+
const wireframeId = `wf-${code}-${i}`;
|
|
3327
|
+
|
|
3328
|
+
return `
|
|
3283
3329
|
<div class="mockup-frame" style="${i > 0 ? 'margin-top:1.5rem;' : ''}">
|
|
3284
3330
|
<div class="mockup-toolbar">
|
|
3285
3331
|
<div class="mockup-dot mockup-dot-red"></div>
|
|
3286
3332
|
<div class="mockup-dot mockup-dot-yellow"></div>
|
|
3287
3333
|
<div class="mockup-dot mockup-dot-green"></div>
|
|
3288
3334
|
<span class="mockup-title">${wf.screen || wf.section}</span>
|
|
3335
|
+
${hasSvg ? `
|
|
3336
|
+
<div class="wireframe-toggle">
|
|
3337
|
+
<button class="wireframe-toggle-btn active" data-target="${wireframeId}" data-view="svg" onclick="toggleWireframeView('${wireframeId}', 'svg')">SVG</button>
|
|
3338
|
+
<button class="wireframe-toggle-btn" data-target="${wireframeId}" data-view="ascii" onclick="toggleWireframeView('${wireframeId}', 'ascii')">ASCII</button>
|
|
3339
|
+
</div>` : ''}
|
|
3289
3340
|
</div>
|
|
3290
|
-
<div class="mockup-content">
|
|
3291
|
-
${
|
|
3292
|
-
? `<
|
|
3293
|
-
|
|
3341
|
+
<div class="mockup-content" id="${wireframeId}">
|
|
3342
|
+
${hasSvg
|
|
3343
|
+
? `<div class="svg-wireframe wireframe-view active" data-view="svg">${wf.svgContent}</div>
|
|
3344
|
+
<pre class="ascii-wireframe wireframe-view" data-view="ascii" style="display:none;">${wf.content || ''}</pre>`
|
|
3345
|
+
: (wf.format === 'ascii'
|
|
3346
|
+
? `<pre class="ascii-wireframe">${wf.content || ''}</pre>`
|
|
3347
|
+
: `<div class="svg-wireframe">${wf.content || ''}</div>`)}
|
|
3294
3348
|
</div>
|
|
3295
3349
|
${wf.description ? `
|
|
3296
3350
|
<div class="wireframe-description">
|
|
@@ -3329,7 +3383,27 @@ function renderModuleMockups(code) {
|
|
|
3329
3383
|
>${getWireframeComment(code, wf.screen)}</textarea>
|
|
3330
3384
|
</div>
|
|
3331
3385
|
</div>
|
|
3332
|
-
`).join('');
|
|
3386
|
+
`}).join('');
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
function toggleWireframeView(wireframeId, view) {
|
|
3390
|
+
const container = document.getElementById(wireframeId);
|
|
3391
|
+
if (!container) return;
|
|
3392
|
+
|
|
3393
|
+
// Toggle visibility of wireframe views
|
|
3394
|
+
container.querySelectorAll('.wireframe-view').forEach(el => {
|
|
3395
|
+
const isTarget = el.dataset.view === view;
|
|
3396
|
+
el.style.display = isTarget ? '' : 'none';
|
|
3397
|
+
el.classList.toggle('active', isTarget);
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
// Toggle button active states
|
|
3401
|
+
const toolbar = container.closest('.mockup-frame').querySelector('.wireframe-toggle');
|
|
3402
|
+
if (toolbar) {
|
|
3403
|
+
toolbar.querySelectorAll('.wireframe-toggle-btn').forEach(btn => {
|
|
3404
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3333
3407
|
}
|
|
3334
3408
|
|
|
3335
3409
|
function getPermRoles() {
|
|
@@ -387,18 +387,30 @@ function renderModuleMockups(code) {
|
|
|
387
387
|
</div>`;
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
return wireframes.map((wf, i) =>
|
|
390
|
+
return wireframes.map((wf, i) => {
|
|
391
|
+
const hasSvg = !!wf.svgContent;
|
|
392
|
+
const wireframeId = `wf-${code}-${i}`;
|
|
393
|
+
|
|
394
|
+
return `
|
|
391
395
|
<div class="mockup-frame" style="${i > 0 ? 'margin-top:1.5rem;' : ''}">
|
|
392
396
|
<div class="mockup-toolbar">
|
|
393
397
|
<div class="mockup-dot mockup-dot-red"></div>
|
|
394
398
|
<div class="mockup-dot mockup-dot-yellow"></div>
|
|
395
399
|
<div class="mockup-dot mockup-dot-green"></div>
|
|
396
400
|
<span class="mockup-title">${wf.screen || wf.section}</span>
|
|
401
|
+
${hasSvg ? `
|
|
402
|
+
<div class="wireframe-toggle">
|
|
403
|
+
<button class="wireframe-toggle-btn active" data-target="${wireframeId}" data-view="svg" onclick="toggleWireframeView('${wireframeId}', 'svg')">SVG</button>
|
|
404
|
+
<button class="wireframe-toggle-btn" data-target="${wireframeId}" data-view="ascii" onclick="toggleWireframeView('${wireframeId}', 'ascii')">ASCII</button>
|
|
405
|
+
</div>` : ''}
|
|
397
406
|
</div>
|
|
398
|
-
<div class="mockup-content">
|
|
399
|
-
${
|
|
400
|
-
? `<
|
|
401
|
-
|
|
407
|
+
<div class="mockup-content" id="${wireframeId}">
|
|
408
|
+
${hasSvg
|
|
409
|
+
? `<div class="svg-wireframe wireframe-view active" data-view="svg">${wf.svgContent}</div>
|
|
410
|
+
<pre class="ascii-wireframe wireframe-view" data-view="ascii" style="display:none;">${wf.content || ''}</pre>`
|
|
411
|
+
: (wf.format === 'ascii'
|
|
412
|
+
? `<pre class="ascii-wireframe">${wf.content || ''}</pre>`
|
|
413
|
+
: `<div class="svg-wireframe">${wf.content || ''}</div>`)}
|
|
402
414
|
</div>
|
|
403
415
|
${wf.description ? `
|
|
404
416
|
<div class="wireframe-description">
|
|
@@ -437,7 +449,27 @@ function renderModuleMockups(code) {
|
|
|
437
449
|
>${getWireframeComment(code, wf.screen)}</textarea>
|
|
438
450
|
</div>
|
|
439
451
|
</div>
|
|
440
|
-
`).join('');
|
|
452
|
+
`}).join('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function toggleWireframeView(wireframeId, view) {
|
|
456
|
+
const container = document.getElementById(wireframeId);
|
|
457
|
+
if (!container) return;
|
|
458
|
+
|
|
459
|
+
// Toggle visibility of wireframe views
|
|
460
|
+
container.querySelectorAll('.wireframe-view').forEach(el => {
|
|
461
|
+
const isTarget = el.dataset.view === view;
|
|
462
|
+
el.style.display = isTarget ? '' : 'none';
|
|
463
|
+
el.classList.toggle('active', isTarget);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Toggle button active states
|
|
467
|
+
const toolbar = container.closest('.mockup-frame').querySelector('.wireframe-toggle');
|
|
468
|
+
if (toolbar) {
|
|
469
|
+
toolbar.querySelectorAll('.wireframe-toggle-btn').forEach(btn => {
|
|
470
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
441
473
|
}
|
|
442
474
|
|
|
443
475
|
function getPermRoles() {
|
|
@@ -61,6 +61,48 @@
|
|
|
61
61
|
.svg-wireframe svg {
|
|
62
62
|
max-width: 100%;
|
|
63
63
|
height: auto;
|
|
64
|
+
display: block;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ============================================
|
|
68
|
+
WIREFRAME VIEW TOGGLE (SVG / ASCII)
|
|
69
|
+
============================================ */
|
|
70
|
+
.wireframe-toggle {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: 2px;
|
|
73
|
+
background: var(--bg-dark);
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
padding: 2px;
|
|
76
|
+
border: 1px solid var(--border);
|
|
77
|
+
margin-left: auto;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.wireframe-toggle-btn {
|
|
81
|
+
padding: 0.25rem 0.6rem;
|
|
82
|
+
font-size: 0.7rem;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
letter-spacing: 0.02em;
|
|
85
|
+
border: none;
|
|
86
|
+
border-radius: 4px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
background: transparent;
|
|
89
|
+
color: var(--text-muted);
|
|
90
|
+
transition: all 0.15s ease;
|
|
91
|
+
font-family: inherit;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.wireframe-toggle-btn:hover {
|
|
95
|
+
color: var(--text-bright);
|
|
96
|
+
background: var(--bg-hover);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.wireframe-toggle-btn.active {
|
|
100
|
+
background: var(--primary);
|
|
101
|
+
color: #ffffff;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.wireframe-view:not(.active) {
|
|
105
|
+
display: none;
|
|
64
106
|
}
|
|
65
107
|
|
|
66
108
|
.wireframe-description {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Module Acceptance Criteria (Shared Reference)
|
|
2
|
+
|
|
3
|
+
> **Loaded by:** step-03d-validate (section 11-POST-CHECK-BASH), step-05a-handoff (pre-handoff gate), ralph-loop step-01-task (input validation)
|
|
4
|
+
> **Purpose:** Define measurable, bash-verifiable acceptance criteria that a module MUST pass before being marked "specified".
|
|
5
|
+
> **Key principle:** These checks read the REAL feature.json file on disk — NOT in-memory data the model "thinks" it wrote.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why Bash-Verifiable?
|
|
10
|
+
|
|
11
|
+
All previous checks were **pseudocode interpreted by the model**. The model can "decide" a check passes even when it fails (hallucination of validation). Bash checks are **objective** — they read the actual file and count real elements.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Acceptance Criteria Table
|
|
16
|
+
|
|
17
|
+
| # | Criterion | Minimum | Field Path | Blocking | Category |
|
|
18
|
+
|---|-----------|---------|------------|----------|----------|
|
|
19
|
+
| AC-01 | Entities present | 1 | `analysis.entities[]` | YES | Data Model |
|
|
20
|
+
| AC-02 | Entity attributes have `type` field | ALL | `analysis.entities[].attributes[].type` | YES | Data Model |
|
|
21
|
+
| AC-03 | Use cases present | 2 | `specification.useCases[]` | YES | Requirements |
|
|
22
|
+
| AC-04 | Functional requirements present | 4 | `specification.functionalRequirements[]` | YES | Requirements |
|
|
23
|
+
| AC-05 | Wireframes present | 1 | `specification.uiWireframes[] \|\| specification.wireframes[]` | YES | UI |
|
|
24
|
+
| AC-06 | Wireframes >= sections | count match | wireframes.length >= sections.length | YES | UI |
|
|
25
|
+
| AC-07 | Wireframe content non-empty | ALL | `wireframe.mockup \|\| wireframe.ascii \|\| wireframe.content` | YES | UI |
|
|
26
|
+
| AC-08 | Sections present | 1 | `specification.sections[]` | YES | UI |
|
|
27
|
+
| AC-09 | SeedDataCore 7 arrays non-empty | 7/7 | `specification.seedDataCore.*` | YES | Seed Data |
|
|
28
|
+
| AC-10 | Gherkin scenarios is array | `Array.isArray` | `specification.gherkinScenarios` | YES | Format |
|
|
29
|
+
| AC-11 | API endpoints present | 1 | `specification.apiEndpoints[]` | YES | API |
|
|
30
|
+
| AC-12 | Messages present | 4 | `specification.messages[]` | YES | Messages |
|
|
31
|
+
| AC-13 | Validations present | 1 | `specification.validations[]` | YES | Validation |
|
|
32
|
+
| AC-14 | Wireframe field naming | canonical | `screen` not `title`, `mockup` not `ascii` | WARNING | Convention |
|
|
33
|
+
| AC-15 | Validation rules format | ALL array | `specification.validations[].rules` is `Array` | YES | Format |
|
|
34
|
+
| AC-16 | Messages have `message` field | ALL present | `specification.messages[].message` truthy | YES | Format |
|
|
35
|
+
| AC-17 | Gherkin content structure | ALL valid | Each element has `feature` (string) + `scenarios` (array) | YES | Format |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Bash Verification Script (node -e)
|
|
40
|
+
|
|
41
|
+
> **Usage:** Called from step-03d section 11-POST-CHECK-BASH after writing module feature.json.
|
|
42
|
+
> Also callable standalone for debugging: `node -e "..." path/to/feature.json`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
MODULE_JSON="{module_feature_json_path}"
|
|
46
|
+
node -e "
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf-8'));
|
|
49
|
+
const spec = data.specification || {};
|
|
50
|
+
const analysis = data.analysis || {};
|
|
51
|
+
const wf = spec.uiWireframes || spec.wireframes || [];
|
|
52
|
+
const sections = spec.sections || [];
|
|
53
|
+
const sdc = spec.seedDataCore || {};
|
|
54
|
+
|
|
55
|
+
const checks = [
|
|
56
|
+
['AC-01: entities >= 1', (analysis.entities||[]).length, 1],
|
|
57
|
+
['AC-03: useCases >= 2', (spec.useCases||[]).length, 2],
|
|
58
|
+
['AC-04: FRs >= 4', (spec.functionalRequirements||[]).length, 4],
|
|
59
|
+
['AC-05: wireframes >= 1', wf.length, 1],
|
|
60
|
+
['AC-06: wireframes >= sections', wf.length, sections.length],
|
|
61
|
+
['AC-08: sections >= 1', sections.length, 1],
|
|
62
|
+
['AC-09: seedDataCore 7 arrays', Object.keys(sdc).filter(k => Array.isArray(sdc[k]) && sdc[k].length > 0).length, 7],
|
|
63
|
+
['AC-10: gherkin is array', Array.isArray(spec.gherkinScenarios) ? 1 : 0, 1],
|
|
64
|
+
['AC-11: apiEndpoints >= 1', (spec.apiEndpoints||[]).length, 1],
|
|
65
|
+
['AC-12: messages >= 4', (spec.messages||[]).length, 4],
|
|
66
|
+
['AC-13: validations >= 1', (spec.validations||[]).length, 1]
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const fails = [];
|
|
70
|
+
checks.forEach(c => {
|
|
71
|
+
if (c[1] < c[2]) {
|
|
72
|
+
fails.push(c);
|
|
73
|
+
console.error('FAIL: ' + c[0] + ' = ' + c[1] + ' (min: ' + c[2] + ')');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// AC-02: Entity attribute types
|
|
78
|
+
const badAttrs = (analysis.entities||[]).flatMap(e =>
|
|
79
|
+
(e.attributes||[]).filter(a => !a.type).map(a => e.name + '.' + a.name)
|
|
80
|
+
);
|
|
81
|
+
if (badAttrs.length > 0) {
|
|
82
|
+
fails.push(['AC-02: attr.type missing', badAttrs.length, 0]);
|
|
83
|
+
console.error('FAIL: AC-02: ' + badAttrs.length + ' attributes without type: ' + badAttrs.slice(0, 5).join(', ') + (badAttrs.length > 5 ? '...' : ''));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// AC-07: Wireframe content non-empty
|
|
87
|
+
const emptyWf = wf.filter(w => !w.mockup && !w.ascii && !w.content);
|
|
88
|
+
if (emptyWf.length > 0) {
|
|
89
|
+
fails.push(['AC-07: wireframe content empty', emptyWf.length, 0]);
|
|
90
|
+
console.error('FAIL: AC-07: ' + emptyWf.length + ' wireframes have EMPTY content');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// AC-14: Wireframe field naming (WARNING only)
|
|
94
|
+
const badFields = wf.filter(w => w.title || w.ascii || (w.name && !w.screen));
|
|
95
|
+
if (badFields.length > 0) {
|
|
96
|
+
console.warn('WARNING: AC-14: ' + badFields.length + ' wireframes use non-canonical field names (title/ascii/name)');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// AC-15: Validation rules format (rules must be array, not string)
|
|
100
|
+
const badRules = (spec.validations||[]).filter(v => v.rules && !Array.isArray(v.rules));
|
|
101
|
+
if (badRules.length > 0) {
|
|
102
|
+
fails.push(['AC-15: validations[].rules not array', badRules.length, 0]);
|
|
103
|
+
console.error('FAIL: AC-15: ' + badRules.length + ' validations have rules as string instead of array');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// AC-16: Messages must have 'message' field
|
|
107
|
+
const noMsg = (spec.messages||[]).filter(m => !m.message);
|
|
108
|
+
if (noMsg.length > 0) {
|
|
109
|
+
fails.push(['AC-16: messages[].message missing', noMsg.length, 0]);
|
|
110
|
+
console.error('FAIL: AC-16: ' + noMsg.length + ' messages missing "message" field');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// AC-17: Gherkin content structure (each element needs feature + scenarios[])
|
|
114
|
+
if (Array.isArray(spec.gherkinScenarios)) {
|
|
115
|
+
const badGherkin = spec.gherkinScenarios.filter(g => !g.feature || !Array.isArray(g.scenarios));
|
|
116
|
+
if (badGherkin.length > 0) {
|
|
117
|
+
fails.push(['AC-17: gherkin content invalid', badGherkin.length, 0]);
|
|
118
|
+
console.error('FAIL: AC-17: ' + badGherkin.length + ' gherkin entries missing feature or scenarios[]');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Summary
|
|
123
|
+
if (fails.length > 0) {
|
|
124
|
+
console.error('\\nBLOCKING: ' + fails.length + ' acceptance criteria FAILED');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
console.log('PASS: All acceptance criteria met (AC-01 to AC-17)');
|
|
128
|
+
" "$MODULE_JSON"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Integration Points
|
|
134
|
+
|
|
135
|
+
### 1. step-03d-validate.md (section 11-POST-CHECK-BASH)
|
|
136
|
+
|
|
137
|
+
After writing module feature.json, run the bash script above.
|
|
138
|
+
IF FAIL → re-execute section 11 write with corrected data → re-run until PASS.
|
|
139
|
+
|
|
140
|
+
### 2. step-05a-handoff.md (pre-handoff gate)
|
|
141
|
+
|
|
142
|
+
Before writing handoff for each module (section 7a step 3), run the bash script on the module feature.json.
|
|
143
|
+
IF FAIL → STOP handoff for this module, report which criteria failed.
|
|
144
|
+
|
|
145
|
+
### 3. ralph-loop step-01-task.md (input validation)
|
|
146
|
+
|
|
147
|
+
At PRD load time, run the bash script on each module feature.json.
|
|
148
|
+
IF FAIL → BLOCKING: "Module {name} does not meet acceptance criteria — run /business-analyse to fix".
|
|
149
|
+
This prevents ralph-loop from generating code from incomplete specifications.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Recovery Actions per Criterion
|
|
154
|
+
|
|
155
|
+
| Criterion | Recovery |
|
|
156
|
+
|-----------|----------|
|
|
157
|
+
| AC-01/02 | Re-run step-03a2-analysis (entity definition) |
|
|
158
|
+
| AC-03/04 | Re-run step-03c-compile sections 8b/8c |
|
|
159
|
+
| AC-05/06/07 | Re-run step-03b-ui (wireframe generation) — step-03b now writes intermediately |
|
|
160
|
+
| AC-08 | Re-run step-03b section 3a-bis (section definition) |
|
|
161
|
+
| AC-09 | Re-run step-03c section 8f-bis (seedDataCore transform) |
|
|
162
|
+
| AC-10 | Auto-fix: wrap in array `[gherkinScenarios]` |
|
|
163
|
+
| AC-11 | Re-run step-03c section 8k |
|
|
164
|
+
| AC-12 | Re-run step-03c section 8i |
|
|
165
|
+
| AC-13 | Re-run step-03c section 8h |
|
|
166
|
+
| AC-14 | Auto-fix: rename title→screen, ascii→mockup (done by step-03c/03d auto-fix) |
|
|
167
|
+
| AC-15 | Auto-fix: wrap string in array `[rules]` (done by step-03c ABSOLUTE FORMAT CHECKS) |
|
|
168
|
+
| AC-16 | Auto-fix: copy `description` → `message` (done by step-03c ABSOLUTE FORMAT CHECKS) |
|
|
169
|
+
| AC-17 | Re-run step-03c section 8g — ensure each gherkin entry has `feature` string + `scenarios` array |
|