@atlashub/smartstack-cli 4.27.0 → 4.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.
@@ -72,16 +72,27 @@ For each module, anticipate the navigation structure:
72
72
  For each section:
73
73
  - code (kebab-case, e.g., "list", "detail", "dashboard")
74
74
  - label (display name)
75
+ - sectionType: primary | functional | view | embedded
76
+ - permissionMode: crud | custom | read-only | inherit
75
77
  - resources: [
76
78
  { code, type (SmartTable|SmartForm|SmartCard|SmartKanban|SmartDashboard|SmartFilter), label }
77
79
  ]
78
80
  ```
79
81
 
82
+ **Section classification rules:**
83
+
84
+ | sectionType | permissionMode | When to use | Examples |
85
+ |---|---|---|---|
86
+ | `primary` | `crud` | Main entry point of the module, visible in menu | list |
87
+ | `functional` | `crud` or `custom` | Independent functional zone with own access control | approve, import, planning |
88
+ | `view` | `inherit` | Subordinate view reached from a primary section | detail, edit |
89
+ | `embedded` | `read-only` | Widget or tab embedded in another section | dashboard (when embedded in module) |
90
+
80
91
  Common patterns:
81
- - **Data-centric module**: list (SmartTable) + detail (SmartForm)
82
- - **Workflow module**: list + detail + kanban (SmartKanban)
83
- - **Reporting module**: dashboard (SmartDashboard) + detail
84
- - **Full module**: list + detail + dashboard
92
+ - **Data-centric module**: list (`primary`/`crud`) + detail (`view`/`inherit`)
93
+ - **Workflow module**: list (`primary`/`crud`) + detail (`view`/`inherit`) + approve (`functional`/`custom`)
94
+ - **Reporting module**: dashboard (`primary`/`read-only`) + detail (`view`/`inherit`)
95
+ - **Full module**: list (`primary`/`crud`) + detail (`view`/`inherit`) + dashboard (`embedded`/`read-only`)
85
96
 
86
97
  ### 4. Dependency Graph
87
98
 
@@ -111,6 +122,9 @@ For EACH identified element, ask yourself:
111
122
  - Does this module need a dashboard?
112
123
  - Is the list/detail pattern sufficient or are there other views?
113
124
  - Are there workflow steps that need dedicated sections?
125
+ - Does this section need its own access control? If not → `view` or `embedded`
126
+ - Is this a read-only view (dashboard, balances, statistics)? If yes → `permissionMode: read-only`
127
+ - Is this section reached by clicking a row in another section? If yes → always `view`
114
128
 
115
129
  **Resources:**
116
130
  - Is SmartTable the right component for this list?
@@ -159,8 +173,8 @@ Write via ba-writer:
159
173
  "priority": "must",
160
174
  "entities": ["Employee", "Contract"],
161
175
  "anticipatedSections": [
162
- { "code": "list", "label": "Liste", "resources": [{ "code": "employees-grid", "type": "SmartTable" }] },
163
- { "code": "detail", "label": "Fiche", "resources": [{ "code": "employee-form", "type": "SmartForm" }] }
176
+ { "code": "list", "label": "Liste", "sectionType": "primary", "permissionMode": "crud", "resources": [{ "code": "employees-grid", "type": "SmartTable" }] },
177
+ { "code": "detail", "label": "Fiche", "sectionType": "view", "permissionMode": "inherit", "resources": [{ "code": "employee-form", "type": "SmartForm" }] }
164
178
  ]
165
179
  }
166
180
  ],
@@ -123,6 +123,10 @@ Define the permission matrix:
123
123
  }
124
124
  ```
125
125
 
126
+ **Auto-detection rules:**
127
+ - If the cadrage mentions "export", "Excel", "CSV", or "télécharger" → automatically add `.export` permission and an export use case (UC-{PREFIX}-EXPORT)
128
+ - If the cadrage mentions "import", "importer", "upload" → automatically add `.import` permission and an import use case (UC-{PREFIX}-IMPORT)
129
+
126
130
  ### F. Interface Specs — Delegated to /ba-design-ui
127
131
 
128
132
  > **Screen specifications are NOT produced in this step.**
@@ -163,6 +167,9 @@ Before advancing to step 04, verify:
163
167
  - [ ] All entity relationships reference existing entities
164
168
  - [ ] All UC business rule references exist
165
169
  - [ ] All permission paths follow the convention
170
+ - [ ] Sections with `sectionType: view` do NOT appear in `permissionPaths` (they inherit from module)
171
+ - [ ] Sections with `permissionMode: read-only` only have `.read` in `permissionPaths` (no create/update/delete)
172
+ - [ ] Sections with `permissionMode: inherit` have ZERO entries in `permissionPaths`
166
173
 
167
174
  ## Transition
168
175
 
@@ -123,6 +123,26 @@ if (actualNavRoute !== permission_path) {
123
123
  console.warn(` Got: "${actualNavRoute}"`);
124
124
  // Warning only — proceed
125
125
  }
126
+
127
+ // Validate segment count
128
+ const segments = actualNavRoute ? actualNavRoute.split('.').length : 0;
129
+ if (segments < 2) {
130
+ console.error(`BLOCKING: NavRoute "${actualNavRoute}" has only ${segments} segment(s) — minimum 2 required`);
131
+ console.error(` Format: "app.module" (2 segments) or "app.module.section" (3 segments)`);
132
+ STOP;
133
+ }
134
+
135
+ // If entity is in a section subfolder, NavRoute must have 3+ segments
136
+ // Check: controller path has 3+ levels after Controllers/ → section-level
137
+ const controllerDir = controllerCall.controllerFile.replace(/[^/]*$/, '');
138
+ const depthAfterControllers = controllerDir.split('Controllers/')[1]?.split('/').filter(Boolean).length || 0;
139
+ if (depthAfterControllers >= 2 && segments < 3) {
140
+ console.warn(`WARNING: Controller is in a section subfolder (depth=${depthAfterControllers})`);
141
+ console.warn(` but NavRoute "${actualNavRoute}" has only ${segments} segments`);
142
+ console.warn(` Expected 3 segments: "app.module.section"`);
143
+ console.warn(` A 2-segment NavRoute on a section controller causes API 404s`);
144
+ // Warning — developer should verify and fix
145
+ }
126
146
  ```
127
147
 
128
148
  ---
@@ -30,6 +30,12 @@ From `usecases.json > useCases[]`:
30
30
 
31
31
  Include: Service per UC cluster, DTOs for API contracts, Validators (FluentValidation), Query handlers
32
32
 
33
+ **Validator generation rules:**
34
+ - Every entity with Create and/or Update use cases MUST have a corresponding Validator
35
+ - Validators MUST be registered in DI (`services.AddScoped<IValidator<CreateXxxDto>, CreateXxxValidator>()`)
36
+ - Validators MUST be injected into controllers/services that handle POST/PUT operations
37
+ - NO TODO/placeholder comments allowed in Validators — all validation rules from business rules (BR-VAL-*) must be implemented
38
+
33
39
  ## 4.3 Infrastructure Files
34
40
 
35
41
  From `entities.json > entities[]`:
@@ -48,7 +54,8 @@ Generated from `usecases.json` + `entities.json`:
48
54
 
49
55
  ```json
50
56
  "api": [
51
- { "path": "src/API/Controllers/{ApplicationName}/{EntityName}Controller.cs", "type": "ApiController", "linkedUCs": [], "linkedFRs": [], "module": "{moduleCode}" }
57
+ { "path": "src/API/Controllers/{ApplicationName}/{EntityName}Controller.cs", "type": "ApiController", "navRoute": "{app-kebab}.{module-kebab}", "isSection": false, "linkedUCs": [], "linkedFRs": [], "module": "{moduleCode}" },
58
+ { "path": "src/API/Controllers/{ApplicationName}/{ModuleName}/{SectionEntityName}Controller.cs", "type": "ApiController", "navRoute": "{app-kebab}.{module-kebab}.{section-kebab}", "isSection": true, "linkedUCs": [], "linkedFRs": [], "module": "{moduleCode}" }
52
59
  ]
53
60
  ```
54
61
 
@@ -80,6 +87,23 @@ From `screens.json > screens[]` and `usecases.json > useCases[]`:
80
87
 
81
88
  **Dashboard acceptance criteria:** Chart library (Recharts), chart types matching spec, KPI cards, filters, CSS variables, responsive layout, wireframe-matching positions.
82
89
 
90
+ ## 4.5b Notification Files (CONDITIONAL)
91
+
92
+ > Generated only when `lifeCycles[].transitions[].effects[]` contains entries with `type: "notification"`.
93
+
94
+ ```json
95
+ "notifications": [
96
+ { "path": "src/Application/Notifications/{ApplicationName}/{ModuleName}/{NotificationName}Notification.cs", "type": "Notification", "linkedUCs": [], "module": "{moduleCode}", "description": "Notification triggered by lifecycle transition" },
97
+ { "path": "src/Application/Notifications/{ApplicationName}/{ModuleName}/{NotificationName}NotificationHandler.cs", "type": "NotificationHandler", "linkedUCs": [], "module": "{moduleCode}", "description": "Handler that sends in-app/email notification" }
98
+ ]
99
+ ```
100
+
101
+ **Generation rules:**
102
+ - One Notification + Handler pair per unique `notification` effect in lifecycle transitions
103
+ - NotificationName derived from transition: `{Entity}{TransitionName}` (e.g., `OrderApproved`)
104
+ - Handler must use `INotificationService` for in-app and `IEmailService` for email type
105
+ - Notification must include: recipient resolution, template reference, and payload mapping
106
+
83
107
  ## 4.6 SeedData Files
84
108
 
85
109
  **OBLIGATORY: 2 app-level CORE + per module CORE (NavigationModule + NavigationSections + Permissions + Roles) + business per module:**
@@ -127,7 +127,9 @@ const seedDataCore = {
127
127
  ? `/${toKebabCase(appCode)}/${toKebabCase(m.code)}/:id`
128
128
  : `/${toKebabCase(appCode)}/${toKebabCase(m.code)}/${toKebabCase(s.code)}`,
129
129
  displayOrder: (j + 1) * 10,
130
- navigation: s.code === "detail" ? "hidden" : "visible"
130
+ navigation: s.code === "detail" ? "hidden" : "visible",
131
+ // Propagate permissionMode for section-level permission generation
132
+ permissionMode: s.permissionMode || (s.code === "detail" ? "inherit" : s.code === "dashboard" ? "read-only" : "crud")
131
133
  }))
132
134
  );
133
135
 
@@ -191,6 +191,129 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
191
191
  }
192
192
  ```
193
193
 
194
+ ## Entity-Level File Completeness Check (BLOCKING)
195
+
196
+ > **LESSON LEARNED (audit ba-002):** Artifact verification checked "at least one file per category"
197
+ > but never reconciled against the **handoff contract** (`prd.implementation.filesToCreate`).
198
+ > Result: 4/17 API endpoints were missing but the category showed "complete" because *some* controllers existed.
199
+
200
+ ```javascript
201
+ // BLOCKING: Verify EVERY file from prd.implementation.filesToCreate exists on disk
202
+ const filesToCreate = prd.implementation?.filesToCreate;
203
+ if (filesToCreate) {
204
+ const handoffMissing = [];
205
+ const handoffPresent = [];
206
+
207
+ for (const [category, files] of Object.entries(filesToCreate)) {
208
+ for (const file of (files || [])) {
209
+ const filePath = file.path || file;
210
+ if (fileExists(filePath)) {
211
+ handoffPresent.push({ category, path: filePath });
212
+ } else {
213
+ handoffMissing.push({ category, path: filePath });
214
+ }
215
+ }
216
+ }
217
+
218
+ const totalHandoff = handoffPresent.length + handoffMissing.length;
219
+ const coveragePct = totalHandoff > 0 ? Math.round((handoffPresent.length / totalHandoff) * 100) : 100;
220
+ console.log(`Handoff file coverage: ${handoffPresent.length}/${totalHandoff} (${coveragePct}%)`);
221
+
222
+ if (handoffMissing.length > 0) {
223
+ console.error(`BLOCKING: ${handoffMissing.length} files from handoff contract missing on disk`);
224
+
225
+ // Group missing files by category for targeted remediation
226
+ const missingByCategory = {};
227
+ for (const m of handoffMissing) {
228
+ if (!missingByCategory[m.category]) missingByCategory[m.category] = [];
229
+ missingByCategory[m.category].push(m.path);
230
+ }
231
+
232
+ // Inject remediation tasks for each missing file
233
+ let maxIdNum = Math.max(...prd.tasks.map(t => {
234
+ const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
235
+ return isNaN(num) ? 0 : num;
236
+ }), 0);
237
+ const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'HNDOFF-';
238
+
239
+ for (const [cat, paths] of Object.entries(missingByCategory)) {
240
+ for (const p of paths) {
241
+ // Skip if a remediation task already exists for this file
242
+ const alreadyExists = prd.tasks.some(t =>
243
+ t.description.includes(p) && t.description.includes('[HANDOFF-REMEDIATION]')
244
+ );
245
+ if (alreadyExists) continue;
246
+
247
+ maxIdNum++;
248
+ prd.tasks.push({
249
+ id: `${prefix}${String(maxIdNum).padStart(3, '0')}`,
250
+ description: `[HANDOFF-REMEDIATION] Create missing file: ${p}`,
251
+ status: 'pending',
252
+ category: cat,
253
+ dependencies: [],
254
+ acceptance_criteria: `File ${p} exists on disk and compiles`,
255
+ started_at: null, completed_at: null, iteration: null,
256
+ commit_hash: null, files_changed: [], validation: null, error: null
257
+ });
258
+ }
259
+ console.error(` ${cat}: ${paths.length} missing — ${paths.map(p => p.split('/').pop()).join(', ')}`);
260
+ }
261
+
262
+ writeJSON(currentPrdPath, prd);
263
+ }
264
+ } // end filesToCreate check
265
+ ```
266
+
267
+ ## Type Reference Verification (Cross-File Integrity)
268
+
269
+ > **LESSON LEARNED (audit ba-002):** Artifact verification counted files but didn't check
270
+ > that types referenced in code actually exist. `EmployeeListDto` was used in services
271
+ > but the file was missing — a "phantom reference" that artifact file-counting missed.
272
+
273
+ ```javascript
274
+ // After artifact checks, verify cross-file type references for completed categories
275
+ if (completedCats.has('application') && completedCats.has('api')) {
276
+ const serviceFiles = glob('src/**/Services/**/*Service.cs');
277
+ const dtoFiles = glob('src/**/DTOs/**/*.cs');
278
+ const entityFiles = glob('src/**/Domain/**/*.cs');
279
+
280
+ // Extract all DTO type names from actual files
281
+ const existingTypes = new Set();
282
+ for (const f of [...dtoFiles, ...entityFiles]) {
283
+ const content = readFile(f);
284
+ const typeMatches = content.matchAll(/(?:class|record|struct|enum|interface)\s+(\w+)/g);
285
+ for (const m of typeMatches) existingTypes.add(m[1]);
286
+ }
287
+
288
+ // Check service files for references to types that don't exist
289
+ const phantomRefs = [];
290
+ for (const f of serviceFiles) {
291
+ const content = readFile(f);
292
+ // Look for Dto/Entity type references in return types and method params
293
+ const typeRefs = content.matchAll(/(?:<|,\s*|\(|new\s+)(\w+(?:Dto|Entity|Exception))\b/g);
294
+ for (const m of typeRefs) {
295
+ if (!existingTypes.has(m[1]) && !m[1].startsWith('I')) {
296
+ phantomRefs.push({ file: f, type: m[1] });
297
+ }
298
+ }
299
+ }
300
+
301
+ if (phantomRefs.length > 0) {
302
+ console.error(`PHANTOM TYPE REFERENCES DETECTED (${phantomRefs.length}):`);
303
+ phantomRefs.forEach(r => console.error(` ${r.file}: references ${r.type} — TYPE DOES NOT EXIST`));
304
+
305
+ // Reset application category tasks to pending — code references non-existent types
306
+ prd.tasks.filter(t => t.category === 'application' && t.status === 'completed')
307
+ .forEach(t => {
308
+ t.status = 'pending';
309
+ t.error = `Phantom type references: ${phantomRefs.map(r=>r.type).join(', ')}`;
310
+ t.completed_at = null;
311
+ });
312
+ writeJSON(currentPrdPath, prd);
313
+ }
314
+ }
315
+ ```
316
+
194
317
  ---
195
318
 
196
319
  ## Key Rules
@@ -198,4 +321,6 @@ for (const [cat, check] of Object.entries(artifactChecks)) {
198
321
  - **Inject EVERY iteration:** Not just once during load
199
322
  - **Frontend depends on seedData:** Not just API
200
323
  - **Check artifacts:** Mark tasks pending if files don't exist
324
+ - **Check type references:** Verify types used in code actually exist (phantom reference detection)
325
+ - **Check handoff files:** Verify EVERY file from `prd.implementation.filesToCreate` exists on disk
201
326
  - **Never skip:** This is the blocker for "missing frontend/test" failures
@@ -289,6 +289,35 @@ if (pending > 0) {
289
289
  console.log(`Apex completed: ${completed}/${batchIds.length} tasks`);
290
290
  ```
291
291
 
292
+ ### B3b. Post-Batch Build Verification (BLOCKING)
293
+
294
+ > **LESSON LEARNED (audit ba-002):** Without build checks between batches, compilation
295
+ > errors from early batches propagate silently through all subsequent batches.
296
+ > The final "Build PASS" was never actually verified.
297
+
298
+ ```bash
299
+ # Quick build check after each batch — BLOCKING if fails
300
+ dotnet build --no-restore --verbosity quiet
301
+ if [ $? -ne 0 ]; then
302
+ echo "BLOCKING: Build fails after batch [${firstCategory}]"
303
+ # Inject immediate fix task — will be picked up next iteration
304
+ fi
305
+ ```
306
+
307
+ ```javascript
308
+ if (BUILD_FAILED) {
309
+ const maxId = Math.max(...updatedPrd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
310
+ updatedPrd.tasks.push({
311
+ id: `FIX-${maxId+1}`,
312
+ description: `BLOCKING: Fix build regression after ${firstCategory} batch`,
313
+ status: 'pending', category: 'validation', dependencies: [],
314
+ acceptance_criteria: 'dotnet build passes with 0 errors'
315
+ });
316
+ writeJSON(currentPrdPath, updatedPrd);
317
+ // Continue to section C (commit) then D (loop) — fix task will be picked up next iteration
318
+ }
319
+ ```
320
+
292
321
  ---
293
322
 
294
323
  ## C. Commit PRD State
@@ -310,27 +339,54 @@ appendFile('.ralph/progress.txt',
310
339
 
311
340
  After apex returns, check for newly skipped tasks:
312
341
 
342
+ > **LESSON LEARNED (audit ba-002):** Individual skips in critical categories (api, test, domain,
343
+ > infrastructure) allowed 4/17 endpoints and 74% stub tests to pass undetected. The old logic
344
+ > only blocked when ALL tasks in a category were skipped — individual skips slipped through.
345
+
313
346
  ```javascript
347
+ const CRITICAL_CATEGORIES = ['api', 'test', 'domain', 'infrastructure'];
348
+ const MAX_SKIP_RETRIES = 3;
349
+
314
350
  const newlySkipped = prdCheck.tasks.filter(t =>
315
351
  batchIds.includes(t.id) && t.status === 'skipped'
316
352
  );
317
353
  if (newlySkipped.length > 0) {
318
354
  console.warn(`⚠ ${newlySkipped.length} tasks were SKIPPED by apex:`);
319
- newlySkipped.forEach(t => console.warn(` - ${t.id}: ${t.description}`));
355
+ newlySkipped.forEach(t => console.warn(` - ${t.id}: ${t.description} [${t.category}]`));
320
356
 
321
- // If ALL tasks in a category were skipped → BLOCKING, reset for retry
322
357
  const skippedCategories = [...new Set(newlySkipped.map(t => t.category))];
323
358
  for (const cat of skippedCategories) {
359
+ const isCritical = CRITICAL_CATEGORIES.includes(cat);
324
360
  const allInCat = prdCheck.tasks.filter(t => t.category === cat);
325
361
  const allSkipped = allInCat.every(t => t.status === 'skipped');
326
- if (allSkipped) {
327
- console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
328
- // Reset to pending for retry
329
- allInCat.forEach(t => {
330
- t.status = 'pending';
331
- t._retryCount = (t._retryCount || 0) + 1;
332
- t.error = `All tasks in category "${cat}" skipped — auto-retry`;
333
- });
362
+
363
+ if (isCritical) {
364
+ // CRITICAL CATEGORY: Block on INDIVIDUAL skips (not just when ALL are skipped)
365
+ const skippedInCat = newlySkipped.filter(t => t.category === cat);
366
+ for (const t of skippedInCat) {
367
+ const retryCount = t._skipRetryCount || 0;
368
+ if (retryCount < MAX_SKIP_RETRIES) {
369
+ console.error(`BLOCKING: Task ${t.id} skipped in critical category "${cat}" — reset to pending (retry ${retryCount + 1}/${MAX_SKIP_RETRIES})`);
370
+ t.status = 'pending';
371
+ t._skipRetryCount = retryCount + 1;
372
+ t._retryCount = (t._retryCount || 0) + 1;
373
+ t.error = `Skipped in critical category "${cat}" — auto-retry ${retryCount + 1}/${MAX_SKIP_RETRIES}`;
374
+ } else {
375
+ console.error(`FAILED: Task ${t.id} skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — marking failed`);
376
+ t.status = 'failed';
377
+ t.error = `Skipped ${MAX_SKIP_RETRIES} times in critical category "${cat}" — max retries exhausted`;
378
+ }
379
+ }
380
+ } else {
381
+ // NON-CRITICAL CATEGORY: Block only when ALL tasks in category are skipped (existing behavior)
382
+ if (allSkipped) {
383
+ console.error(`BLOCKING: ALL tasks in category "${cat}" were skipped — investigate root cause`);
384
+ allInCat.forEach(t => {
385
+ t.status = 'pending';
386
+ t._retryCount = (t._retryCount || 0) + 1;
387
+ t.error = `All tasks in category "${cat}" skipped — auto-retry`;
388
+ });
389
+ }
334
390
  }
335
391
  }
336
392
  writeJSON(currentPrdPath, prdCheck);
@@ -27,6 +27,27 @@ if (fileExists(queuePath)) {
27
27
 
28
28
  // All categories present — module is complete
29
29
  // Continue below...
30
+
31
+ // HANDOFF FILE GATE (double-security — step-04 §1.8 is primary, this is defense-in-depth)
32
+ // The build gate does NOT detect missing files if nothing references them (no compile error).
33
+ const ftc = prd.implementation?.filesToCreate;
34
+ if (ftc) {
35
+ let ftcMissing = 0;
36
+ const ftcDetails = [];
37
+ for (const [cat, files] of Object.entries(ftc)) {
38
+ for (const file of (files || [])) {
39
+ if (!fileExists(file.path || file)) {
40
+ ftcMissing++;
41
+ ftcDetails.push(`${cat}: ${(file.path || file).split('/').pop()}`);
42
+ }
43
+ }
44
+ }
45
+ if (ftcMissing > 0) {
46
+ console.error(`BLOCKING: ${ftcMissing} handoff files missing — cannot advance module`);
47
+ ftcDetails.forEach(d => console.error(` ${d}`));
48
+ return; // Back to compact loop — remediation tasks already injected by category-completeness
49
+ }
50
+ }
30
51
  }
31
52
  ```
32
53
 
@@ -36,6 +57,36 @@ if (fileExists(queuePath)) {
36
57
 
37
58
  ```javascript
38
59
  if (missing.length === 0) {
60
+ // MANDATORY BUILD+TEST GATE before advancing module
61
+ // (audit ba-002: module marked "100% complete" despite build errors and security flaws)
62
+ const buildOk = execSync('dotnet build --no-restore --verbosity quiet').exitCode === 0;
63
+ if (!buildOk) {
64
+ console.error(`BLOCKING: Module ${currentModule.code} build fails — cannot advance to next module`);
65
+ const maxId = Math.max(...prd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
66
+ prd.tasks.push({
67
+ id: `GATE-${maxId+1}`, description: `BLOCKING: Fix build for module ${currentModule.code}`,
68
+ status: 'pending', category: 'validation', dependencies: [],
69
+ acceptance_criteria: 'dotnet build passes with 0 errors'
70
+ });
71
+ writeJSON(currentPrdPath, prd);
72
+ return; // Back to compact loop — do NOT advance
73
+ }
74
+
75
+ const testOk = execSync('dotnet test --no-build --verbosity quiet').exitCode === 0;
76
+ if (!testOk) {
77
+ console.error(`BLOCKING: Module ${currentModule.code} tests fail — cannot advance to next module`);
78
+ const maxId = Math.max(...prd.tasks.map(t => parseInt(t.id.replace(/\D/g,''))||0));
79
+ prd.tasks.push({
80
+ id: `GATE-${maxId+1}`, description: `BLOCKING: Fix tests for module ${currentModule.code}`,
81
+ status: 'pending', category: 'validation', dependencies: [],
82
+ acceptance_criteria: 'dotnet test passes with 0 failures'
83
+ });
84
+ writeJSON(currentPrdPath, prd);
85
+ return; // Back to compact loop — do NOT advance
86
+ }
87
+
88
+ console.log(`MODULE GATE PASSED: ${currentModule.code} — build OK, tests OK`);
89
+
39
90
  // Mark current module done
40
91
  currentModule.status = 'completed';
41
92
  queue.completedModules++;
@@ -68,6 +119,15 @@ if (missing.length === 0) {
68
119
 
69
120
  console.log(`MODULE COMPLETE: ${currentModule.code} → NEXT: ${queue.modules[nextIndex].code}`);
70
121
 
122
+ // CONTEXT REFRESH: Force /compact between modules to prevent context degradation
123
+ // (audit ba-002: quality degraded progressively — module 2 had more errors than module 1
124
+ // because patterns from module 1 were lost during context compaction)
125
+ console.log('Forcing /compact for fresh context before next module...');
126
+ // The /compact command clears accumulated tool results and intermediate reasoning,
127
+ // preserving only key decisions and the current state. This ensures the next module
128
+ // starts with a clean context window instead of degraded fragments from the previous module.
129
+ // INVOKE /compact
130
+
71
131
  // Return to step-01 to load next module's tasks
72
132
  // (step-01 will detect module-changed.json and load next PRD)
73
133
  return GO_TO_STEP_01;