@atlashub/smartstack-cli 3.31.0 → 3.32.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/.documentation/installation.html +7 -2
- package/README.md +7 -1
- package/dist/index.js +33 -37
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +547 -97
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/health-check.sh +2 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +10 -7
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +132 -124
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +4 -4
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +38 -15
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +20 -8
- package/templates/skills/apex/SKILL.md +7 -9
- package/templates/skills/apex/_shared.md +9 -2
- package/templates/skills/apex/references/code-generation.md +412 -0
- package/templates/skills/apex/references/post-checks.md +377 -37
- package/templates/skills/apex/references/smartstack-api.md +229 -5
- package/templates/skills/apex/references/smartstack-frontend.md +368 -11
- package/templates/skills/apex/references/smartstack-layers.md +54 -7
- package/templates/skills/apex/steps/step-00-init.md +1 -2
- package/templates/skills/apex/steps/step-01-analyze.md +45 -2
- package/templates/skills/apex/steps/step-02-plan.md +23 -2
- package/templates/skills/apex/steps/step-03-execute.md +195 -5
- package/templates/skills/apex/steps/step-04-examine.md +18 -5
- package/templates/skills/apex/steps/step-05-deep-review.md +9 -11
- package/templates/skills/apex/steps/step-06-resolve.md +5 -9
- package/templates/skills/apex/steps/step-07-tests.md +66 -1
- package/templates/skills/apex/steps/step-08-run-tests.md +12 -3
- package/templates/skills/application/references/provider-template.md +62 -39
- package/templates/skills/application/templates-backend.md +3 -3
- package/templates/skills/application/templates-frontend.md +12 -12
- package/templates/skills/application/templates-seed.md +14 -4
- package/templates/skills/business-analyse/SKILL.md +9 -6
- package/templates/skills/business-analyse/questionnaire/04-data.md +8 -0
- package/templates/skills/business-analyse/references/agent-module-prompt.md +84 -5
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +83 -19
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +6 -2
- package/templates/skills/business-analyse/references/team-orchestration.md +443 -110
- package/templates/skills/business-analyse/references/validation-checklist.md +5 -4
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +44 -0
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +72 -1
- package/templates/skills/business-analyse/steps/step-03c-compile.md +93 -7
- package/templates/skills/business-analyse/steps/step-03d-validate.md +34 -2
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +40 -0
- package/templates/skills/controller/references/controller-code-templates.md +2 -2
- package/templates/skills/controller/templates.md +12 -12
- package/templates/skills/feature-full/steps/step-01-implementation.md +4 -4
- package/templates/skills/ralph-loop/references/category-rules.md +44 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +37 -0
- package/templates/skills/ralph-loop/references/core-seed-data.md +51 -20
- package/templates/skills/review-code/references/owasp-api-top10.md +1 -1
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
| Validate | MCP `validate_conventions` |
|
|
18
18
|
|
|
19
19
|
**Rules:**
|
|
20
|
-
- Inherit `BaseEntity`,
|
|
20
|
+
- Inherit `BaseEntity`, choose tenant interface based on data isolation requirements:
|
|
21
|
+
- **`ITenantEntity`** (strict mode, default): User can only access their tenant's data. Implement `Guid TenantId { get; set; }`. Service MUST filter by `_currentTenant.TenantId` with guard clause.
|
|
22
|
+
- **`IOptionalTenantEntity`** (optional mode): Cross-tenant data (shared lookup tables, public content). Implement `Guid? TenantId { get; set; }`. Service MAY accept null TenantId. EF global filter handles both shared (null) and tenant-scoped (TenantId == current) rows automatically.
|
|
23
|
+
- **`IScopedTenantEntity`** (scoped mode): Data scoped by Scope + TenantId combination. Validate both match current context in service layer.
|
|
24
|
+
- **No tenant interface** (none mode): Cross-tenant system data (no isolation). No TenantId property, no filtering needed.
|
|
25
|
+
- Also implement `IAuditableEntity` for CreatedBy/UpdatedBy tracking
|
|
21
26
|
- See `references/smartstack-api.md` for exact BaseEntity API (Id, CreatedAt, UpdatedAt only)
|
|
22
27
|
- No Code/IsDeleted/RowVersion in BaseEntity — add business properties yourself
|
|
23
28
|
- Domain events for state changes
|
|
@@ -62,8 +67,12 @@
|
|
|
62
67
|
| Validate | MCP `validate_conventions` |
|
|
63
68
|
|
|
64
69
|
**Rules:**
|
|
65
|
-
- **
|
|
66
|
-
- **
|
|
70
|
+
- **Tenant injection depends on entity tenant mode** (see entity rules below):
|
|
71
|
+
- **strict mode (default):** Inject `ICurrentTenantService` + guard clause: `var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();` (mandatory, returns 400)
|
|
72
|
+
- **optional mode:** Inject `ICurrentTenantService`, no guard: `var tenantId = _currentTenant.TenantId;` (nullable). EF global filter handles shared+tenant data.
|
|
73
|
+
- **scoped mode:** Inject `ICurrentTenantService`, validate Scope + TenantId consistency. Tenant scope requires TenantId.
|
|
74
|
+
- **none mode:** No `ICurrentTenantService` injection needed (cross-tenant data, no filtering)
|
|
75
|
+
- **ALL services MUST inject `ICurrentUserService`** for audit trails
|
|
67
76
|
- **ALL services MUST inject `ILogger<T>`** for production diagnostics
|
|
68
77
|
- CQRS with MediatR
|
|
69
78
|
- FluentValidation for all commands
|
|
@@ -272,7 +281,19 @@ const [loading, setLoading] = useState(true);
|
|
|
272
281
|
### Components & CSS
|
|
273
282
|
|
|
274
283
|
**Components:** SmartTable, SmartFilter, EntityCard, SmartForm, StatCard (NEVER raw HTML)
|
|
275
|
-
**CSS:** Variables
|
|
284
|
+
**CSS:** Variables ONLY — hardcoded Tailwind colors are **BLOCKING** in POST-CHECK 13:
|
|
285
|
+
|
|
286
|
+
| Instead of | Use |
|
|
287
|
+
|-----------|-----|
|
|
288
|
+
| `bg-white` | `bg-[var(--bg-card)]` |
|
|
289
|
+
| `bg-gray-50` | `bg-[var(--bg-primary)]` |
|
|
290
|
+
| `text-gray-900` | `text-[var(--text-primary)]` |
|
|
291
|
+
| `text-gray-500` | `text-[var(--text-secondary)]` |
|
|
292
|
+
| `border-gray-200` | `border-[var(--border-color)]` |
|
|
293
|
+
| `bg-blue-600` | `bg-[var(--color-accent-500)]` |
|
|
294
|
+
| `hover:bg-blue-700` | `hover:bg-[var(--color-accent-600)]` |
|
|
295
|
+
| `text-red-500` | `text-[var(--error-text)]` |
|
|
296
|
+
|
|
276
297
|
**Loader:** `Loader2` from `lucide-react` for spinners, `PageLoader` for Suspense fallback
|
|
277
298
|
|
|
278
299
|
### Section Routes (when module has sections)
|
|
@@ -291,10 +312,10 @@ Section pages live in `src/pages/{ContextPascal}/{AppPascal}/{Module}/{Section}/
|
|
|
291
312
|
|
|
292
313
|
### FK Fields in Forms (CRITICAL)
|
|
293
314
|
|
|
294
|
-
Any entity property that is a FK Guid (e.g., `EmployeeId`, `DepartmentId`) MUST use the `EntityLookup` component — NEVER a
|
|
315
|
+
Any entity property that is a FK Guid (e.g., `EmployeeId`, `DepartmentId`) MUST use the `EntityLookup` component — NEVER a `<select>` dropdown, NEVER a `<input type="text">`. Users cannot type GUIDs manually, and `<select>` fails at scale (no search, loads all options).
|
|
295
316
|
|
|
296
317
|
```tsx
|
|
297
|
-
// CORRECT — EntityLookup for FK field
|
|
318
|
+
// CORRECT — EntityLookup for FK field (ONLY acceptable pattern)
|
|
298
319
|
<EntityLookup
|
|
299
320
|
apiEndpoint="/api/business/human-resources/employees"
|
|
300
321
|
value={formData.employeeId}
|
|
@@ -306,6 +327,12 @@ Any entity property that is a FK Guid (e.g., `EmployeeId`, `DepartmentId`) MUST
|
|
|
306
327
|
|
|
307
328
|
// WRONG — plain text input for FK GUID
|
|
308
329
|
<input type="text" value={formData.employeeId} placeholder="Enter employee ID" />
|
|
330
|
+
|
|
331
|
+
// WRONG — <select> dropdown for FK field (even with API-loaded options)
|
|
332
|
+
<select value={formData.departmentId} onChange={(e) => setFormData({...formData, departmentId: e.target.value})}>
|
|
333
|
+
<option value="">Select...</option>
|
|
334
|
+
{departments.map(d => <option key={d.id} value={d.id}>{d.name}</option>)}
|
|
335
|
+
</select>
|
|
309
336
|
```
|
|
310
337
|
|
|
311
338
|
**Backend requirement:** Each target entity's GetAll endpoint MUST accept `?search=` parameter. See `smartstack-api.md` Service Pattern.
|
|
@@ -317,6 +344,7 @@ See `references/smartstack-frontend.md` section 6 for the full `EntityLookup` co
|
|
|
317
344
|
- `import axios` → use `@/services/api/apiClient`
|
|
318
345
|
- `<table>` → use SmartTable
|
|
319
346
|
- `<input type="text">` for FK Guid fields → use `EntityLookup`
|
|
347
|
+
- `<select>` for FK Guid fields → use `EntityLookup` (even with API-loaded options)
|
|
320
348
|
- Placeholder "Enter ID" or "Enter GUID" → use `EntityLookup`
|
|
321
349
|
- Hardcoded Tailwind colors (`bg-white`, `text-gray-900`) → use CSS variables
|
|
322
350
|
- Static page imports in route files → use `React.lazy()`
|
|
@@ -346,7 +374,26 @@ t('{module}:actions.create', 'Create entity') // ALWAYS with namespace prefix
|
|
|
346
374
|
|
|
347
375
|
---
|
|
348
376
|
|
|
349
|
-
## Layer
|
|
377
|
+
## Layer 2b — Documentation (after frontend pages exist)
|
|
378
|
+
|
|
379
|
+
> **After frontend pages are created, generate module documentation.**
|
|
380
|
+
|
|
381
|
+
| Action | Tool |
|
|
382
|
+
|--------|------|
|
|
383
|
+
| Generate doc-data.ts + page wrapper | `/documentation` skill (type: user) |
|
|
384
|
+
| Verify DocToggleButton in pages | POST-CHECK 29 |
|
|
385
|
+
|
|
386
|
+
**Output files:**
|
|
387
|
+
- `src/pages/docs/business/{app}/{module}/doc-data.ts` — data-driven documentation
|
|
388
|
+
- `src/pages/docs/business/{app}/{module}/index.tsx` — page wrapper using `DocRenderer`
|
|
389
|
+
- `src/i18n/locales/fr/docs-{app}-{module}.json` — French doc translations
|
|
390
|
+
|
|
391
|
+
**DocToggleButton (MANDATORY):** Every list/detail page must include `DocToggleButton` in its header.
|
|
392
|
+
See `references/smartstack-frontend.md` section 7 for the component pattern.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Layer 3 — Tests (sequential)
|
|
350
397
|
|
|
351
398
|
| Action | Tool |
|
|
352
399
|
|--------|------|
|
|
@@ -28,7 +28,7 @@ Extract flags from the raw input:
|
|
|
28
28
|
```
|
|
29
29
|
Input: "{raw_input}"
|
|
30
30
|
|
|
31
|
-
Flags to detect: -a, -x, -s, -
|
|
31
|
+
Flags to detect: -a, -x, -s, -e, -r, -pr
|
|
32
32
|
Remaining text after flag removal = {task_description}
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -38,7 +38,6 @@ Remaining text after flag removal = {task_description}
|
|
|
38
38
|
auto_mode: false
|
|
39
39
|
examine_mode: false
|
|
40
40
|
save_mode: false
|
|
41
|
-
test_mode: false
|
|
42
41
|
economy_mode: false
|
|
43
42
|
pr_mode: false
|
|
44
43
|
resume_mode: false
|
|
@@ -111,8 +111,51 @@ For each entity found (or to be created), identify FK relationships:
|
|
|
111
111
|
- Record: `{ entity, fkProperty, targetEntity, isRequired }`
|
|
112
112
|
|
|
113
113
|
**Why:** FK fields drive two critical requirements:
|
|
114
|
-
1. **Frontend:** Each FK field MUST use
|
|
115
|
-
2. **Backend:** Each target entity's GetAll MUST support `?search=`
|
|
114
|
+
1. **Frontend:** Each FK field MUST use `<EntityLookup />` (NEVER `<input>`, NEVER `<select>` — even with API-loaded options)
|
|
115
|
+
2. **Backend:** Each target entity's GetAll MUST support `?search=` parameter + return `PaginatedResult<T>`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 4a. Tenant Mode Decision
|
|
120
|
+
|
|
121
|
+
For each entity found (or to be created), determine the `tenantMode`:
|
|
122
|
+
|
|
123
|
+
- **`strict`** (default) — Data belongs to a specific tenant. Examples: Employee, Order, Invoice, CustomerInteraction
|
|
124
|
+
- **`optional`** — Data can be shared across tenants OR tenant-specific. Examples: Department, Currency, JobTitle, Language
|
|
125
|
+
- **`scoped`** — Data has explicit visibility scope controlled by workflow/feature rules. Examples: Settings, Workflow, EmailTemplate, ApprovalConfig
|
|
126
|
+
- **`none`** — Platform-wide data (never tenant-filtered). Examples: Navigation, Permission, User, SystemConfiguration
|
|
127
|
+
|
|
128
|
+
**Why:** Tenant mode drives EF query filters, seed data generation, and API access control. Must be decided before planning.
|
|
129
|
+
|
|
130
|
+
**Reference:** See `references/smartstack-api.md` for TenantId handling patterns and `smartstack-layers.md` for seed data strategies per tenant mode.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 4b. Code Pattern Detection
|
|
135
|
+
|
|
136
|
+
For each entity found (or to be created), identify code generation needs:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
IF feature.json exists AND entity has codePattern:
|
|
140
|
+
→ Use codePattern config from feature.json (strategy, prefix, digits, etc.)
|
|
141
|
+
→ Record in analysis: entity has auto-generated code
|
|
142
|
+
|
|
143
|
+
ELSE IF feature.json exists BUT entity has NO codePattern:
|
|
144
|
+
→ Record: entity needs codePattern decision in step-02
|
|
145
|
+
→ Apply heuristic default based on entity name:
|
|
146
|
+
Invoice/Order/Receipt → timestamp-daily
|
|
147
|
+
Ticket/Incident → timestamp-minute
|
|
148
|
+
Contract/Policy → year-sequential
|
|
149
|
+
Reference/Category → uuid-short
|
|
150
|
+
Employee/Customer/Project → sequential
|
|
151
|
+
|
|
152
|
+
ELSE (no feature.json):
|
|
153
|
+
→ Propose codePattern during step-02 planning based on task description
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Why:** Code generation strategy drives CreateDto structure (Code field removed when auto-generated), service injection (ICodeGenerator<T>), and DI registration. Must be decided before planning.
|
|
157
|
+
|
|
158
|
+
**Reference:** See `references/code-generation.md` for strategy table, volume-to-digits calculation, and backend patterns.
|
|
116
159
|
|
|
117
160
|
---
|
|
118
161
|
|
|
@@ -32,6 +32,17 @@ For each element identified in step-01 (create or modify), assign to a SmartStac
|
|
|
32
32
|
| 2 | I18n (translations) | Parallel |
|
|
33
33
|
| 3 | Tests | Sequential |
|
|
34
34
|
|
|
35
|
+
**Entity Definition Template:** Each entity in the plan MUST include:
|
|
36
|
+
```
|
|
37
|
+
Entity: {EntityName}
|
|
38
|
+
- tenantMode: strict | optional | scoped | none
|
|
39
|
+
- codePattern: auto-generated strategy (if applicable)
|
|
40
|
+
- fkFields: [{field, targetEntity}] (if applicable)
|
|
41
|
+
- acceptance criteria: [AC1, AC2, ...]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `tenantMode` decision (from step-01, section 4a) drives EF query filters, seed data approach, and API authorization. See `smartstack-layers.md` for tenant mode seed data strategies.
|
|
45
|
+
|
|
35
46
|
---
|
|
36
47
|
|
|
37
48
|
## 2. Assign Skill/MCP per File
|
|
@@ -56,6 +67,7 @@ For EACH file in the plan, specify HOW it will be created/modified:
|
|
|
56
67
|
| 6 | Api/Controllers/.../MyController.cs | create | /controller skill |
|
|
57
68
|
| 7 | Seeding/Data/NavigationApplicationSeedData.cs | create | ref smartstack-layers.md (once per app) |
|
|
58
69
|
| 7b | Seeding/Data/ApplicationRolesSeedData.cs | create | ref smartstack-layers.md (once per app) |
|
|
70
|
+
| 7c | Infrastructure/Services/CodeGeneration/ | create | ref code-generation.md (ICodeGenerator<T> + DI, per entity with codePattern != manual) |
|
|
59
71
|
| 8 | Seeding/Data/.../NavigationModuleSeedData.cs | create | ref smartstack-layers.md |
|
|
60
72
|
| 8b | Application/Authorization/Permissions.cs | create | MCP generate_permissions → static constants |
|
|
61
73
|
| 9 | Seeding/Data/.../PermissionsSeedData.cs | create | MCP generate_permissions |
|
|
@@ -76,7 +88,14 @@ For EACH file in the plan, specify HOW it will be created/modified:
|
|
|
76
88
|
|
|
77
89
|
**FK Field Guidance:** If step-01 identified `fkFields[]`, every Create/Edit page MUST use `EntityLookup` for those fields (see `smartstack-frontend.md` section 6). The corresponding backend GetAll endpoints (Layer 1) MUST support `?search=` parameter.
|
|
78
90
|
|
|
79
|
-
### Layer
|
|
91
|
+
### Layer 2b — Documentation (after frontend pages exist)
|
|
92
|
+
|
|
93
|
+
| # | File | Action | Tool |
|
|
94
|
+
|---|------|--------|------|
|
|
95
|
+
| 14b | src/pages/docs/business/{app}/{module}/doc-data.ts | create | /documentation skill |
|
|
96
|
+
| 14c | src/pages/docs/business/{app}/{module}/index.tsx | create | /documentation skill |
|
|
97
|
+
|
|
98
|
+
### Layer 3 — Tests (sequential)
|
|
80
99
|
|
|
81
100
|
| # | File | Action | Tool |
|
|
82
101
|
|---|------|--------|------|
|
|
@@ -114,6 +133,8 @@ Verify the plan respects dependencies:
|
|
|
114
133
|
```
|
|
115
134
|
- Migration AFTER EF configs (always)
|
|
116
135
|
- Application services AFTER domain entities (always)
|
|
136
|
+
- Code generator service AFTER entity + EF config (needs DbSet for sequence query)
|
|
137
|
+
- CreateDto adjustments AFTER code pattern decision (remove Code property if auto-generated)
|
|
117
138
|
- Controllers AFTER application services (always)
|
|
118
139
|
- Frontend AFTER API controllers (API contract needed)
|
|
119
140
|
- Seed data AFTER navigation module exists
|
|
@@ -130,7 +151,7 @@ feat({module}): [domain+infra] {description}
|
|
|
130
151
|
feat({module}): [app+api] {description}
|
|
131
152
|
feat({module}): [frontend] {description}
|
|
132
153
|
feat({module}): [seed] {description}
|
|
133
|
-
feat({module}): [tests] {description}
|
|
154
|
+
feat({module}): [tests] {description}
|
|
134
155
|
```
|
|
135
156
|
|
|
136
157
|
---
|
|
@@ -69,12 +69,32 @@ dotnet build
|
|
|
69
69
|
|
|
70
70
|
Execute each item from the plan sequentially using skills and MCP.
|
|
71
71
|
|
|
72
|
+
### Code Generation (if entities have codePattern != "manual")
|
|
73
|
+
|
|
74
|
+
For each entity with auto-generated code pattern (from feature.json or step-02 decisions):
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
1. Create CodePatternConfig for the entity (strategy, prefix, digits from codePattern)
|
|
78
|
+
2. Register ICodeGenerator<TEntity> in DependencyInjection.cs (Infrastructure layer)
|
|
79
|
+
→ See references/code-generation.md for DI registration pattern
|
|
80
|
+
3. Update CreateDto: REMOVE Code property (auto-generated, not user-provided)
|
|
81
|
+
4. Update CreateDtoValidator: REMOVE Code regex rule (not in DTO anymore)
|
|
82
|
+
5. Update service CreateAsync: inject ICodeGenerator<TEntity>, call NextCodeAsync()
|
|
83
|
+
→ Code is auto-generated BEFORE entity creation
|
|
84
|
+
→ See references/code-generation.md for service integration pattern
|
|
85
|
+
6. Keep Code in ResponseDto (returned to frontend after creation)
|
|
86
|
+
7. Keep Code in UpdateDto ONLY if code is mutable (rare — discuss with user)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**CRITICAL:** If `codePattern.strategy == "manual"` (or no codePattern), keep the current behavior:
|
|
90
|
+
Code stays in CreateDto, user provides it, validator has regex rule.
|
|
91
|
+
|
|
72
92
|
**For frontend tasks (economy_mode):** Follow the same rules as exec-frontend above:
|
|
73
93
|
- `MCP scaffold_api_client` → API client + types + React Query hook
|
|
74
94
|
- `MCP scaffold_routes` with `outputFormat: 'clientRoutes'` for lazy imports
|
|
75
95
|
- Pages: use `/ui-components` skill — MANDATORY for entity lists, grids, tables, dashboards
|
|
76
96
|
- **Form pages: Create `EntityCreatePage.tsx` (route: `/create`) and `EntityEditPage.tsx` (route: `/:id/edit`)**
|
|
77
|
-
- **FK FIELDS (CRITICAL):** Any Guid FK property (e.g., EmployeeId, DepartmentId) MUST use `EntityLookup` component — NEVER a
|
|
97
|
+
- **FK FIELDS (CRITICAL):** Any Guid FK property (e.g., EmployeeId, DepartmentId) MUST use `EntityLookup` component — NEVER a `<select>` dropdown, NEVER a `<input type="text">`. A `<select>` loaded from API state is NOT a substitute for EntityLookup. See `smartstack-frontend.md` section 6.
|
|
78
98
|
- **ZERO modals/popups/drawers for forms — ALL forms are full pages with their own URL**
|
|
79
99
|
- **Form tests: Generate `EntityCreatePage.test.tsx` and `EntityEditPage.test.tsx` (co-located)**
|
|
80
100
|
- **SECTION PERMISSIONS:** After calling `MCP generate_permissions` for the module navRoute (3 segments: `{context}.{app}.{module}`), also call it for EACH section navRoute (4 segments: `{context}.{app}.{module}.{section}`)
|
|
@@ -127,9 +147,11 @@ Spawn 2 teammates (Opus, full tools):
|
|
|
127
147
|
→ Back button with navigate(-1) on every form page
|
|
128
148
|
→ See smartstack-frontend.md section 3b for templates
|
|
129
149
|
- **FK FIELDS (CRITICAL):** Foreign key Guid fields (e.g., EmployeeId) MUST use EntityLookup:
|
|
130
|
-
→ NEVER render FK fields as
|
|
131
|
-
→
|
|
150
|
+
→ NEVER render FK fields as `<input>` — users cannot type GUIDs
|
|
151
|
+
→ NEVER render FK fields as `<select>` — even with API-loaded options, `<select>` is NOT acceptable
|
|
152
|
+
→ ONLY use `<EntityLookup />` from @/components/ui/EntityLookup for searchable selection
|
|
132
153
|
→ Each FK field needs: apiEndpoint, mapOption (display name), search support on backend
|
|
154
|
+
→ FORBIDDEN: `<select value={formData.departmentId}>`, `<option value={dept.id}>`, `e.target.value` for FK fields
|
|
133
155
|
→ See smartstack-frontend.md section 6 for the full EntityLookup pattern
|
|
134
156
|
- **FORM TESTS (MANDATORY):**
|
|
135
157
|
→ EntityCreatePage.test.tsx (co-located next to page)
|
|
@@ -173,6 +195,28 @@ shutdown_request → shutdown_response → TeamDelete("apex-exec")
|
|
|
173
195
|
|
|
174
196
|
---
|
|
175
197
|
|
|
198
|
+
## Layer 2b — Documentation (after frontend pages exist)
|
|
199
|
+
|
|
200
|
+
> **After frontend pages are created, generate module documentation via `/documentation` skill.**
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
Invoke /documentation {module_code} --type user
|
|
204
|
+
|
|
205
|
+
This generates:
|
|
206
|
+
- src/pages/docs/business/{app}/{module}/doc-data.ts (data file)
|
|
207
|
+
- src/pages/docs/business/{app}/{module}/index.tsx (page wrapper)
|
|
208
|
+
- src/i18n/locales/fr/docs-{app}-{module}.json (French doc translations)
|
|
209
|
+
- Updates App.tsx routing for doc page
|
|
210
|
+
- Updates docs-manifest.json
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Verify DocToggleButton presence in list/detail pages:**
|
|
214
|
+
- Every `*ListPage.tsx` and `*DetailPage.tsx` MUST import and render `DocToggleButton` in their header
|
|
215
|
+
- Import: `import { DocToggleButton } from '@/components/docs/DocToggleButton';`
|
|
216
|
+
- Placement: inside the header actions area (see `smartstack-frontend.md` section 7)
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
176
220
|
## Layer 2 — Final Seed Data + Validation (sequential)
|
|
177
221
|
|
|
178
222
|
After Layer 1 completes:
|
|
@@ -192,14 +236,160 @@ After Layer 1 completes:
|
|
|
192
236
|
|
|
193
237
|
---
|
|
194
238
|
|
|
239
|
+
## FRONTEND COMPLIANCE GATE (MANDATORY before commit)
|
|
240
|
+
|
|
241
|
+
> **BLOCKING:** Do NOT commit frontend changes until ALL checks pass.
|
|
242
|
+
> These issues are the most common failures in generated code — verify EVERY item.
|
|
243
|
+
|
|
244
|
+
**Run these checks on ALL generated/modified `.tsx` files BEFORE creating the frontend commit:**
|
|
245
|
+
|
|
246
|
+
### Gate 1: CSS Variables (Theme System)
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Check for hardcoded Tailwind colors — MUST use CSS variables
|
|
250
|
+
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
251
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
252
|
+
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
|
|
253
|
+
if [ -n "$HARDCODED" ]; then
|
|
254
|
+
echo "FAIL: Hardcoded Tailwind colors found — must use CSS variables"
|
|
255
|
+
echo "$HARDCODED"
|
|
256
|
+
else
|
|
257
|
+
echo "PASS: CSS variables"
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**If hardcoded colors found, replace BEFORE committing:**
|
|
263
|
+
- `bg-white` → `bg-[var(--bg-card)]`
|
|
264
|
+
- `bg-gray-50` → `bg-[var(--bg-primary)]`
|
|
265
|
+
- `text-gray-900` → `text-[var(--text-primary)]`
|
|
266
|
+
- `text-gray-500/600` → `text-[var(--text-secondary)]`
|
|
267
|
+
- `border-gray-200` → `border-[var(--border-color)]`
|
|
268
|
+
- `bg-blue-600` / `text-blue-600` → `bg-[var(--color-accent-500)]` / `text-[var(--color-accent-500)]`
|
|
269
|
+
- `hover:bg-blue-700` → `hover:bg-[var(--color-accent-600)]`
|
|
270
|
+
- `text-red-500` → `text-[var(--error-text)]`
|
|
271
|
+
- `bg-green-500` → `bg-[var(--success-bg)]`
|
|
272
|
+
|
|
273
|
+
### Gate 2: Forms as Pages (ZERO Modals/Drawers/Slide-overs)
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# Check for modal/dialog/drawer/slide-over imports and inline form patterns — FORBIDDEN
|
|
277
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
278
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
279
|
+
FAIL=false
|
|
280
|
+
|
|
281
|
+
# 2a. Component imports
|
|
282
|
+
MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
|
|
283
|
+
if [ -n "$MODAL_IMPORTS" ]; then
|
|
284
|
+
echo "FAIL: Modal/Dialog/Drawer component imports — forms MUST be full pages"
|
|
285
|
+
echo "$MODAL_IMPORTS"
|
|
286
|
+
FAIL=true
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# 2b. State variables for inline forms (catches drawers/slide-overs without imports)
|
|
290
|
+
MODAL_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
|
|
291
|
+
if [ -n "$MODAL_STATE" ]; then
|
|
292
|
+
echo "FAIL: Inline form state detected — forms MUST be separate page components with own routes"
|
|
293
|
+
echo "$MODAL_STATE"
|
|
294
|
+
FAIL=true
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
if [ "$FAIL" = true ]; then
|
|
298
|
+
echo ""
|
|
299
|
+
echo "Fix: Create EntityCreatePage.tsx (route: /create) and EntityEditPage.tsx (route: /:id/edit)"
|
|
300
|
+
echo "See smartstack-frontend.md section 3b"
|
|
301
|
+
else
|
|
302
|
+
echo "PASS: No modals/drawers"
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**If modals/drawers found:** Replace with separate `EntityCreatePage.tsx` (route: `/{module}/create`) and `EntityEditPage.tsx` (route: `/{module}/:id/edit`). See `smartstack-frontend.md` section 3b.
|
|
308
|
+
|
|
309
|
+
### Gate 3: I18n File Structure
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Verify translation files exist as separate JSON per language
|
|
313
|
+
if [ ! -d "src/i18n/locales" ]; then
|
|
314
|
+
echo "FAIL: Missing src/i18n/locales/ directory — create it with 4 languages"
|
|
315
|
+
else
|
|
316
|
+
for LANG in fr en it de; do
|
|
317
|
+
JSON_FILES=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
|
|
318
|
+
if [ "$JSON_FILES" -eq 0 ]; then
|
|
319
|
+
echo "FAIL: No JSON files in src/i18n/locales/$LANG/"
|
|
320
|
+
else
|
|
321
|
+
echo "PASS: $LANG ($JSON_FILES files)"
|
|
322
|
+
fi
|
|
323
|
+
done
|
|
324
|
+
fi
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**If i18n structure wrong:** Create `src/i18n/locales/{fr,en,it,de}/{module}.json` following the template in `smartstack-frontend.md` section 2. NEVER embed translations in a single `.ts` file.
|
|
328
|
+
|
|
329
|
+
### Gate 4: Lazy Loading
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Check for static page imports in route/App files
|
|
333
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
334
|
+
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
|
|
335
|
+
if [ -n "$APP_TSX" ]; then
|
|
336
|
+
STATIC_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" "$APP_TSX" $ROUTE_FILES 2>/dev/null)
|
|
337
|
+
if [ -n "$STATIC_IMPORTS" ]; then
|
|
338
|
+
echo "FAIL: Static page imports in App.tsx/routes — MUST use React.lazy()"
|
|
339
|
+
echo "$STATIC_IMPORTS"
|
|
340
|
+
else
|
|
341
|
+
echo "PASS: Lazy loading"
|
|
342
|
+
fi
|
|
343
|
+
fi
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Gate 5: useTranslation in Pages
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# Verify pages use i18n
|
|
350
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v "\.test\." | grep -v node_modules)
|
|
351
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
352
|
+
TOTAL=$(echo "$PAGE_FILES" | wc -l)
|
|
353
|
+
WITH_I18N=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
|
|
354
|
+
if [ "$WITH_I18N" -eq 0 ]; then
|
|
355
|
+
echo "FAIL: No pages use useTranslation — all text must be translated"
|
|
356
|
+
else
|
|
357
|
+
echo "PASS: $WITH_I18N/$TOTAL pages use useTranslation"
|
|
358
|
+
fi
|
|
359
|
+
fi
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
> **ALL 5 gates MUST pass before creating the frontend commit.** If ANY gate fails, fix the issues first.
|
|
363
|
+
|
|
364
|
+
### Explicit I18n File Creation
|
|
365
|
+
|
|
366
|
+
When creating i18n files, generate EXACTLY this structure:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
src/i18n/locales/
|
|
370
|
+
├── fr/{module}.json ← French (primary)
|
|
371
|
+
├── en/{module}.json ← English
|
|
372
|
+
├── it/{module}.json ← Italian
|
|
373
|
+
└── de/{module}.json ← German
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Each file MUST contain these keys: `title`, `description`, `actions`, `labels`, `columns`, `form`, `errors`, `validation`, `messages`, `empty`. See `smartstack-frontend.md` section 2 for the complete JSON template.
|
|
377
|
+
|
|
378
|
+
**When delegating to `/ui-components`:** ALWAYS include these explicit instructions in the delegation context:
|
|
379
|
+
- "CSS: Use CSS variables ONLY — `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`. NEVER use hardcoded Tailwind colors like `bg-white` or `text-gray-900`."
|
|
380
|
+
- "Forms: Create/Edit forms are FULL PAGES with own routes (e.g., `/create`, `/:id/edit`). NEVER use modals/dialogs."
|
|
381
|
+
- "I18n: ALL text must use `t('namespace:key', 'Fallback')`. Generate JSON files in `src/i18n/locales/`."
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
195
385
|
## Commits Per Layer
|
|
196
386
|
|
|
197
|
-
After each layer completes and builds pass:
|
|
387
|
+
After each layer completes, gates pass, and builds pass:
|
|
198
388
|
|
|
199
389
|
```
|
|
200
390
|
Layer 0: feat({module}): [domain+infra] {short description}
|
|
201
391
|
Layer 1: feat({module}): [app+api] {short description}
|
|
202
|
-
Layer 1: feat({module}): [frontend] {short description} # if applicable
|
|
392
|
+
Layer 1: feat({module}): [frontend] {short description} # if applicable — AFTER all 5 gates pass
|
|
203
393
|
Layer 2: feat({module}): [seed] {short description} # if applicable
|
|
204
394
|
```
|
|
205
395
|
|
|
@@ -178,8 +178,8 @@ AC2: {criterion} → PASS / FAIL (evidence: {file:line or test})
|
|
|
178
178
|
| Frontend | routes, lazy loading, forms as pages, i18n (4 langs + keys) | PASS / N/A |
|
|
179
179
|
| Security | TenantId filter, RequirePermission, no Guid.Empty, no !.Value | PASS / N/A |
|
|
180
180
|
| Seed data | completeness, deterministic GUIDs, no ContextId/RoleId constants | PASS / N/A |
|
|
181
|
-
| Code quality | PaginatedResult, EntityLookup, CSS vars, search param | PASS / N/A |
|
|
182
|
-
| POST-CHECKs |
|
|
181
|
+
| Code quality | PaginatedResult, EntityLookup (no FK select/input), CSS vars, search param | PASS / N/A |
|
|
182
|
+
| POST-CHECKs | 30 checks from references/post-checks.md | PASS / N/A |
|
|
183
183
|
| Acceptance criteria | AC1..ACn | {X}/{Y} PASS |
|
|
184
184
|
```
|
|
185
185
|
|
|
@@ -191,10 +191,23 @@ Write to `{output_dir}/04-examine.md` with validation results.
|
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
194
|
-
## 10. Route to Next Step
|
|
194
|
+
## 10. Route to Next Step — TESTS ARE MANDATORY
|
|
195
|
+
|
|
196
|
+
> **CRITICAL — DO NOT STOP HERE.**
|
|
197
|
+
> Step-04 (eXamine) is NOT the final step. Tests MUST be scaffolded and run.
|
|
198
|
+
> Completing APEX without tests violates the methodology — skip is FORBIDDEN.
|
|
195
199
|
|
|
196
200
|
```
|
|
197
|
-
examine_mode
|
|
201
|
+
IF examine_mode (-x flag):
|
|
202
|
+
→ Load steps/step-05-deep-review.md (adversarial review)
|
|
203
|
+
→ Then step-06 (resolve) if BLOCKING findings
|
|
204
|
+
→ Then step-07 (tests) — ALWAYS
|
|
205
|
+
|
|
206
|
+
ELSE:
|
|
207
|
+
→ Load steps/step-07-tests.md — IMMEDIATELY after eXamine completes
|
|
198
208
|
```
|
|
199
209
|
|
|
200
|
-
**
|
|
210
|
+
**BLOCKING RULE:** The APEX workflow is NOT complete until steps 07-08 execute.
|
|
211
|
+
The success criteria require: "Tests: 100% pass, >= 80% coverage" — this is impossible without step-07.
|
|
212
|
+
|
|
213
|
+
**NEXT ACTION:** Load `steps/step-07-tests.md` now.
|
|
@@ -61,9 +61,10 @@ For each changed file, check:
|
|
|
61
61
|
- [ ] Correct Layout wrapper per context
|
|
62
62
|
|
|
63
63
|
**FK Fields & Forms:**
|
|
64
|
-
- [ ] FK Guid fields use `EntityLookup` component (NEVER
|
|
65
|
-
- [ ] No
|
|
66
|
-
- [ ]
|
|
64
|
+
- [ ] FK Guid fields use `EntityLookup` component (NEVER `<input>`, NEVER `<select>`)
|
|
65
|
+
- [ ] No `<select>` with `<option>` elements for FK fields (even API-loaded options)
|
|
66
|
+
- [ ] No placeholder text asking user to "Enter ID", "Enter GUID", or "Select {Entity}..."
|
|
67
|
+
- [ ] No inline forms in drawers/slide-overs — Create/Edit forms are full pages with own routes (ZERO modals/dialogs/drawers)
|
|
67
68
|
- [ ] Backend GetAll endpoints support `?search=` param for EntityLookup
|
|
68
69
|
- [ ] Each EntityLookup has `apiEndpoint`, `mapOption`, `label` props
|
|
69
70
|
|
|
@@ -118,14 +119,11 @@ Write to `{output_dir}/05-deep-review.md` with all findings.
|
|
|
118
119
|
|
|
119
120
|
```
|
|
120
121
|
IF BLOCKING findings exist:
|
|
121
|
-
→ Load steps/step-06-resolve.md
|
|
122
|
-
|
|
123
|
-
ELSE IF test_mode = true:
|
|
124
|
-
→ Load steps/step-07-tests.md
|
|
125
|
-
|
|
126
|
-
ELSE IF pr_mode = true:
|
|
127
|
-
→ Create PR and show final summary
|
|
122
|
+
→ Load steps/step-06-resolve.md (fix BLOCKING findings first)
|
|
123
|
+
→ Then step-07-tests.md (tests are MANDATORY)
|
|
128
124
|
|
|
129
125
|
ELSE:
|
|
130
|
-
→
|
|
126
|
+
→ Load steps/step-07-tests.md — IMMEDIATELY
|
|
131
127
|
```
|
|
128
|
+
|
|
129
|
+
> **REMINDER:** Tests are MANDATORY. Do NOT stop after Deep Review — proceed to step-07.
|
|
@@ -92,14 +92,10 @@ Write to `{output_dir}/06-resolve.md` with resolution log.
|
|
|
92
92
|
|
|
93
93
|
```
|
|
94
94
|
IF remaining BLOCKING > 0:
|
|
95
|
-
→ Loop: fix remaining, re-validate
|
|
95
|
+
→ Loop: fix remaining, re-validate (max 3 iterations)
|
|
96
|
+
→ If stuck, ask user before continuing
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
→ Load steps/step-07-tests.md
|
|
99
|
-
|
|
100
|
-
ELSE IF pr_mode = true:
|
|
101
|
-
→ Create PR and show final summary
|
|
102
|
-
|
|
103
|
-
ELSE:
|
|
104
|
-
→ Show final summary and exit
|
|
98
|
+
→ Load steps/step-07-tests.md — MANDATORY, do NOT skip
|
|
105
99
|
```
|
|
100
|
+
|
|
101
|
+
> **REMINDER:** Tests are MANDATORY. After resolving all findings, proceed to step-07 immediately.
|
|
@@ -6,12 +6,77 @@ prev_step: steps/step-06-resolve.md
|
|
|
6
6
|
next_step: steps/step-08-run-tests.md
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# Step 7: Tests
|
|
9
|
+
# Step 7: Tests
|
|
10
|
+
|
|
11
|
+
> **MANDATORY STEP — This is NOT optional.** APEX requires tests for completion.
|
|
12
|
+
> If you reached this step, you MUST scaffold and run tests before finishing.
|
|
10
13
|
|
|
11
14
|
**Goal:** Scaffold comprehensive tests using MCP tools. Target: >= 80% coverage.
|
|
12
15
|
|
|
13
16
|
---
|
|
14
17
|
|
|
18
|
+
## 0. Ensure Frontend Test Tooling Exists (if frontend was generated)
|
|
19
|
+
|
|
20
|
+
> **Before scaffolding frontend tests, the test infrastructure must be in place.**
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Check if Vitest is installed
|
|
24
|
+
WEB_DIR=$(find . -name "package.json" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null | head -1)
|
|
25
|
+
if [ -n "$WEB_DIR" ]; then
|
|
26
|
+
cd "$WEB_DIR"
|
|
27
|
+
|
|
28
|
+
# Install test dependencies if missing
|
|
29
|
+
if ! grep -q '"vitest"' package.json 2>/dev/null; then
|
|
30
|
+
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @types/testing-library__jest-dom
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Create vitest.config.ts if missing
|
|
34
|
+
if [ ! -f "vitest.config.ts" ]; then
|
|
35
|
+
cat > vitest.config.ts << 'VITEST_EOF'
|
|
36
|
+
import { defineConfig } from 'vitest/config';
|
|
37
|
+
import react from '@vitejs/plugin-react';
|
|
38
|
+
import path from 'path';
|
|
39
|
+
|
|
40
|
+
export default defineConfig({
|
|
41
|
+
plugins: [react()],
|
|
42
|
+
test: {
|
|
43
|
+
globals: true,
|
|
44
|
+
environment: 'jsdom',
|
|
45
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
46
|
+
css: true,
|
|
47
|
+
include: ['src/**/*.test.{ts,tsx}'],
|
|
48
|
+
},
|
|
49
|
+
resolve: {
|
|
50
|
+
alias: {
|
|
51
|
+
'@': path.resolve(__dirname, './src'),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
VITEST_EOF
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Create test setup file if missing
|
|
59
|
+
mkdir -p src/test
|
|
60
|
+
if [ ! -f "src/test/setup.ts" ]; then
|
|
61
|
+
cat > src/test/setup.ts << 'SETUP_EOF'
|
|
62
|
+
import '@testing-library/jest-dom';
|
|
63
|
+
SETUP_EOF
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Add test script to package.json if missing
|
|
67
|
+
if ! grep -q '"test"' package.json 2>/dev/null; then
|
|
68
|
+
# Use npm pkg set for safe JSON manipulation
|
|
69
|
+
npm pkg set scripts.test="vitest run"
|
|
70
|
+
npm pkg set scripts.test:watch="vitest"
|
|
71
|
+
npm pkg set scripts.test:coverage="vitest run --coverage"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
cd -
|
|
75
|
+
fi
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
15
80
|
## 1. Ensure Test Projects Exist
|
|
16
81
|
|
|
17
82
|
### 1a. Unit Test Project
|