Skip to content

Commit 29ff84e

Browse files
authored
fix: preserve result aggregator falsy defaults (#611) (#669)
1 parent 524c12c commit 29ff84e

7 files changed

Lines changed: 187 additions & 25 deletions

File tree

.aiox-core/core/execution/result-aggregator.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ class ResultAggregator extends EventEmitter {
1515
super();
1616

1717
// Root path for reports
18-
this.rootPath = config.rootPath || process.cwd();
19-
this.reportDir = config.reportDir || path.join(this.rootPath, 'plan');
18+
this.rootPath = config.rootPath ?? process.cwd();
19+
this.reportDir = config.reportDir ?? path.join(this.rootPath, 'plan');
2020

2121
// Conflict detection settings
2222
this.detectConflicts = config.detectConflicts !== false;
2323

2424
// Aggregation history
2525
this.history = [];
26-
this.maxHistory = config.maxHistory || 50;
26+
this.maxHistory = config.maxHistory ?? 50;
2727
}
2828

2929
/**
@@ -36,8 +36,8 @@ class ResultAggregator extends EventEmitter {
3636

3737
const aggregation = {
3838
id: `agg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
39-
waveIndex: waveResults.waveIndex || waveResults.wave,
40-
startedAt: waveResults.startedAt || new Date().toISOString(),
39+
waveIndex: waveResults.waveIndex ?? waveResults.wave,
40+
startedAt: waveResults.startedAt ?? new Date().toISOString(),
4141
completedAt: new Date().toISOString(),
4242
tasks: [],
4343
conflicts: [],
@@ -50,11 +50,13 @@ class ResultAggregator extends EventEmitter {
5050
for (const result of results) {
5151
aggregation.tasks.push({
5252
taskId: result.taskId,
53-
agentId: result.agentId || 'unknown',
53+
agentId: result.agentId ?? 'unknown',
5454
success: result.success,
55-
filesModified: result.filesModified || this.extractFilesFromOutput(result.output),
56-
duration: result.duration || 0,
57-
output: this.summarizeOutput(result.output || result.result?.output),
55+
filesModified: Array.isArray(result.filesModified)
56+
? result.filesModified
57+
: this.extractFilesFromOutput(result.output ?? result.result?.output),
58+
duration: result.duration ?? 0,
59+
output: this.summarizeOutput(result.output ?? result.result?.output),
5860
error: result.error,
5961
});
6062
}
@@ -366,7 +368,7 @@ class ResultAggregator extends EventEmitter {
366368
* @returns {Promise<string>} - Report file path
367369
*/
368370
async generateReport(aggregation, filename = null) {
369-
const reportName = filename || `wave-results-${aggregation.waveIndex || 'all'}.json`;
371+
const reportName = filename || `wave-results-${aggregation.waveIndex ?? 'all'}.json`;
370372
const reportPath = path.join(this.reportDir, reportName);
371373

372374
// Ensure directory exists
@@ -394,7 +396,7 @@ class ResultAggregator extends EventEmitter {
394396

395397
let md = '# Wave Results Report\n\n';
396398
md += `> **Generated:** ${aggregation.completedAt}\n`;
397-
md += `> **Success Rate:** ${metrics.successRate || metrics.overallSuccessRate}%\n\n`;
399+
md += `> **Success Rate:** ${metrics.successRate ?? metrics.overallSuccessRate}%\n\n`;
398400

399401
md += '## Summary\n\n';
400402
md += '| Metric | Value |\n';
@@ -403,8 +405,8 @@ class ResultAggregator extends EventEmitter {
403405
md += `| Successful | ${metrics.successful} |\n`;
404406
md += `| Failed | ${metrics.failed} |\n`;
405407
md += `| Duration | ${Math.round(metrics.totalDuration / 1000)}s |\n`;
406-
md += `| Conflicts | ${metrics.conflictCount || metrics.totalConflicts || 0} |\n`;
407-
md += `| Files Modified | ${metrics.filesModified || 'N/A'} |\n\n`;
408+
md += `| Conflicts | ${metrics.conflictCount ?? metrics.totalConflicts ?? 0} |\n`;
409+
md += `| Files Modified | ${metrics.filesModified ?? 'N/A'} |\n\n`;
408410

409411
// Tasks section
410412
if (aggregation.tasks && aggregation.tasks.length > 0) {
@@ -414,7 +416,7 @@ class ResultAggregator extends EventEmitter {
414416

415417
for (const task of aggregation.tasks) {
416418
const status = task.success ? '✅' : '❌';
417-
const duration = task.duration ? `${Math.round(task.duration / 1000)}s` : '-';
419+
const duration = task.duration != null ? `${Math.round(task.duration / 1000)}s` : '-';
418420
md += `| ${task.taskId} | ${task.agentId} | ${status} | ${duration} |\n`;
419421
}
420422
md += '\n';

.aiox-core/data/entity-registry.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
metadata:
22
version: 1.0.0
3-
lastUpdated: '2026-05-07T14:31:05.482Z'
3+
lastUpdated: '2026-05-07T15:02:59.792Z'
44
entityCount: 746
55
checksumAlgorithm: sha256
66
resolutionRate: 100
@@ -8822,8 +8822,8 @@ entities:
88228822
score: 0.4
88238823
constraints: []
88248824
extensionPoints: []
8825-
checksum: sha256:fc662bbc649bf704a8f6e70d0203fab389c664a6b4b2932510f84c251ae26610
8826-
lastVerified: '2026-03-11T00:48:55.878Z'
8825+
checksum: sha256:bcd102ca46e74d61af151aa7877921807c9bf5aba45bfba73f9f5c8cf714d8e2
8826+
lastVerified: '2026-05-07T15:02:59.785Z'
88278827
semantic-merge-engine:
88288828
path: .aiox-core/core/execution/semantic-merge-engine.js
88298829
layer: L1

.aiox-core/install-manifest.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# - SHA256 hashes for change detection
88
# - File types for categorization
99
#
10-
version: 5.1.8
11-
generated_at: "2026-05-07T14:31:25.534Z"
10+
version: 5.1.9
11+
generated_at: "2026-05-07T15:10:49.402Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1103
1414
files:
@@ -429,9 +429,9 @@ files:
429429
type: core
430430
size: 9033
431431
- path: core/execution/result-aggregator.js
432-
hash: sha256:fc662bbc649bf704a8f6e70d0203fab389c664a6b4b2932510f84c251ae26610
432+
hash: sha256:bcd102ca46e74d61af151aa7877921807c9bf5aba45bfba73f9f5c8cf714d8e2
433433
type: core
434-
size: 14553
434+
size: 14643
435435
- path: core/execution/semantic-merge-engine.js
436436
hash: sha256:26f2a057407cf114a0611944960a8e6d58d93c5e03abe905489e6b699cb98a75
437437
type: core
@@ -1229,7 +1229,7 @@ files:
12291229
type: data
12301230
size: 9590
12311231
- path: data/entity-registry.yaml
1232-
hash: sha256:277cd2b2973a8b33b7b512794106c6e5e8a5536ed8aecc3c269f7c8ae84c9cf0
1232+
hash: sha256:16fcaa219a4c1364337ae2d534494fe4f058570ba6f3db797e373d5cd0d7e672
12331233
type: data
12341234
size: 522368
12351235
- path: data/learned-patterns.yaml
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# STORY-123.13: Preservar valores falsy válidos no ResultAggregator
2+
3+
## Status
4+
5+
Done
6+
7+
## Story
8+
9+
Como operador do executor paralelo, quero que o ResultAggregator preserve valores válidos como `waveIndex: 0`, `duration: 0`, `filesModified: 0` e strings vazias intencionais, para que relatórios, histórico e markdown não tratem dados reais como ausentes.
10+
11+
## Acceptance Criteria
12+
13+
- [x] AC1. `waveIndex: 0` é preservado durante `aggregate()` e no nome padrão do relatório.
14+
- [x] AC2. Defaults usam fallback apenas para `null`/`undefined` quando valores falsy são válidos.
15+
- [x] AC3. `filesModified` continua seguro: valores não-array não quebram a agregação e caem para extração por output.
16+
- [x] AC4. Markdown renderiza métricas zero em vez de trocar para valores alternativos ou `N/A`.
17+
- [x] AC5. Há regressões automatizadas cobrindo o comportamento.
18+
19+
## Tasks
20+
21+
- [x] Atualizar defaults falsy-safe em `.aiox-core/core/execution/result-aggregator.js`.
22+
- [x] Adicionar regressões focadas em `tests/core/result-aggregator.test.js`.
23+
- [x] Bump de patch para `@aiox-squads/core@5.1.9`.
24+
- [x] Regenerar manifest antes do PR.
25+
26+
## Dev Notes
27+
28+
- Esta story substitui o escopo útil do PR histórico #611 sem reaproveitar o branch antigo, que estava com CI/manifest obsoleto e review pendente.
29+
- O ajuste evita uma troca mecânica cega de `||` por `??`: campos que precisam ser array continuam normalizados antes do uso.
30+
31+
## Validation
32+
33+
- `node -c .aiox-core/core/execution/result-aggregator.js` -> PASS.
34+
- `npm test -- tests/core/result-aggregator.test.js --runInBand` -> PASS, 1 suite / 36 tests.
35+
- `npm test -- tests/core/result-aggregator.test.js tests/core/wave-executor.test.js tests/core/build-state-manager.test.js --runInBand --forceExit` -> PASS, 3 suites / 117 tests.
36+
- `npm run validate:manifest` -> PASS.
37+
- `npm run validate:publish` -> PASS.
38+
- `npm run lint -- --quiet` -> PASS.
39+
- `npm run typecheck` -> PASS.
40+
- `git diff --check` -> PASS.
41+
- `npm run test:ci` -> PASS, 315 suites / 7.846 tests, 149 skipped.
42+
43+
## File List
44+
45+
- `.aiox-core/core/execution/result-aggregator.js`
46+
- `tests/core/result-aggregator.test.js`
47+
- `docs/stories/epic-123/STORY-123.13-result-aggregator-falsy-defaults.md`
48+
- `package.json`
49+
- `package-lock.json`
50+
- `.aiox-core/install-manifest.yaml`
51+
- `.aiox-core/data/entity-registry.yaml`

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aiox-squads/core",
3-
"version": "5.1.8",
3+
"version": "5.1.9",
44
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
55
"bin": {
66
"aiox": "bin/aiox.js",

tests/core/result-aggregator.test.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ describe('ResultAggregator', () => {
4343
expect(ra.maxHistory).toBe(10);
4444
});
4545

46+
test('preserves falsy but intentional config values', () => {
47+
const ra = new ResultAggregator({ rootPath: '', reportDir: '', maxHistory: 0 });
48+
expect(ra.rootPath).toBe('');
49+
expect(ra.reportDir).toBe('');
50+
expect(ra.maxHistory).toBe(0);
51+
});
52+
4653
test('extends EventEmitter', () => {
4754
const ra = new ResultAggregator();
4855
expect(typeof ra.on).toBe('function');
@@ -145,13 +152,64 @@ describe('ResultAggregator', () => {
145152
expect(ra.history.length).toBe(1);
146153
});
147154

155+
test('preserves wave zero and other falsy task values', async () => {
156+
const ra = new ResultAggregator({ rootPath: tmpDir });
157+
const result = await ra.aggregate({
158+
waveIndex: 0,
159+
startedAt: '',
160+
results: [
161+
{
162+
taskId: 't0',
163+
agentId: '',
164+
success: true,
165+
duration: 0,
166+
output: '',
167+
result: { output: 'fallback should not be used' },
168+
filesModified: [],
169+
},
170+
],
171+
});
172+
173+
expect(result.waveIndex).toBe(0);
174+
expect(result.startedAt).toBe('');
175+
expect(result.tasks[0]).toMatchObject({
176+
agentId: '',
177+
duration: 0,
178+
output: '',
179+
filesModified: [],
180+
});
181+
});
182+
183+
test('falls back safely when filesModified is not an array', async () => {
184+
const ra = new ResultAggregator({ rootPath: tmpDir });
185+
const result = await ra.aggregate({
186+
waveIndex: 1,
187+
results: [
188+
{
189+
taskId: 't1',
190+
success: true,
191+
filesModified: false,
192+
output: 'Created `src/app.js`',
193+
},
194+
],
195+
});
196+
197+
expect(result.tasks[0].filesModified).toEqual(['src/app.js']);
198+
});
199+
148200
test('trims history to maxHistory', async () => {
149201
const ra = new ResultAggregator({ rootPath: tmpDir, maxHistory: 2 });
150202
await ra.aggregate({ waveIndex: 1, results: [] });
151203
await ra.aggregate({ waveIndex: 2, results: [] });
152204
await ra.aggregate({ waveIndex: 3, results: [] });
153205
expect(ra.history.length).toBe(2);
154206
});
207+
208+
test('allows maxHistory zero without retaining entries', async () => {
209+
const ra = new ResultAggregator({ rootPath: tmpDir, maxHistory: 0 });
210+
await ra.aggregate({ waveIndex: 1, results: [] });
211+
expect(ra.history.length).toBe(0);
212+
});
155213
});
156214

157215
// ── aggregateAll ──────────────────────────────────────────────────────
@@ -303,6 +361,30 @@ describe('ResultAggregator', () => {
303361
expect(fs.existsSync(reportPath)).toBe(true);
304362
expect(fs.existsSync(reportPath.replace('.json', '.md'))).toBe(true);
305363
});
364+
365+
test('uses wave zero in default report filename', async () => {
366+
const reportDir = path.join(tmpDir, 'plan');
367+
const ra = new ResultAggregator({ reportDir });
368+
const agg = {
369+
waveIndex: 0,
370+
completedAt: new Date().toISOString(),
371+
tasks: [],
372+
conflicts: [],
373+
warnings: [],
374+
metrics: {
375+
totalTasks: 0,
376+
successful: 0,
377+
failed: 0,
378+
successRate: 100,
379+
totalDuration: 0,
380+
conflictCount: 0,
381+
filesModified: 0,
382+
},
383+
};
384+
385+
const reportPath = await ra.generateReport(agg);
386+
expect(path.basename(reportPath)).toBe('wave-results-0.json');
387+
});
306388
});
307389

308390
// ── formatMarkdown ────────────────────────────────────────────────────
@@ -335,6 +417,33 @@ describe('ResultAggregator', () => {
335417
expect(md).toContain('Conflicts');
336418
expect(md).toContain('app.js');
337419
});
420+
421+
test('renders zero metrics instead of falling back to alternate labels', () => {
422+
const ra = new ResultAggregator();
423+
const agg = {
424+
completedAt: new Date().toISOString(),
425+
tasks: [{ taskId: 't0', agentId: '@dev', success: true, duration: 0 }],
426+
conflicts: [],
427+
warnings: [],
428+
metrics: {
429+
totalTasks: 1,
430+
successful: 1,
431+
failed: 0,
432+
successRate: 0,
433+
overallSuccessRate: 99,
434+
totalDuration: 0,
435+
conflictCount: 0,
436+
totalConflicts: 12,
437+
filesModified: 0,
438+
},
439+
};
440+
441+
const md = ra.formatMarkdown(agg);
442+
expect(md).toContain('> **Success Rate:** 0%');
443+
expect(md).toContain('| Conflicts | 0 |');
444+
expect(md).toContain('| Files Modified | 0 |');
445+
expect(md).toContain('| t0 | @dev | ✅ | 0s |');
446+
});
338447
});
339448

340449
// ── History ───────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)