@@ -146,16 +146,14 @@ module MassPanic {
146146 var results = allResults[ 0 ..#actualCount] ;
147147 sort(results, comparator= new ResultComparator());
148148
149- // Filter to only repos with findings if --findingsOnly
150- var filteredResults = results;
151- if findingsOnly {
152- var kept: list(RepoResult);
153- for r in results {
154- if r.weakPointCount > 0 || r.error != " " then
155- kept.pushBack(r);
156- }
157- filteredResults = kept.toArray();
149+ // Filter to only repos with findings if --findingsOnly.
150+ // Always build via list to avoid Chapel array-shape mismatch on assignment.
151+ var filteredList: list(RepoResult);
152+ for r in results {
153+ if !findingsOnly || r.weakPointCount > 0 || r.error != " " then
154+ filteredList.pushBack(r);
158155 }
156+ var filteredResults = filteredList.toArray();
159157
160158 // Build system image
161159 var image = buildSystemImage(filteredResults, repos.size);
@@ -242,9 +240,8 @@ module MassPanic {
242240 return ;
243241 }
244242
245- // Build minimal SystemImage objects from snapshot summaries —
246- // global metrics are accurate; per-node breakdown is not available
247- // without loading the full image JSON.
243+ // Build SystemImage objects. Start with summary metrics (always available),
244+ // then load per-node data from saved image files when paths are present.
248245 var olderImg: SystemImage;
249246 olderImg.generatedAt = fromSnap.timestamp;
250247 olderImg.globalHealth = fromSnap.globalHealth;
@@ -263,6 +260,28 @@ module MassPanic {
263260 newerImg.reposScanned = toSnap.reposScanned;
264261 newerImg.nodeCount = toSnap.nodeCount;
265262
263+ // Load per-node data if image files are available (written by takeSnapshot).
264+ // Older snapshots written before this feature won't have imagePath set — the
265+ // diff will still work with summary-only data, just no per-node breakdown.
266+ if fromSnap.imagePath != " " {
267+ const olderNodes = loadImageNodes(fromSnap.imagePath);
268+ if olderNodes.size > 0 {
269+ olderImg.nodes = olderNodes;
270+ if !quiet then
271+ writeln (" mass-panic diff: loaded " , olderNodes.size,
272+ " nodes from " , fromSnap.id);
273+ }
274+ }
275+ if toSnap.imagePath != " " {
276+ const newerNodes = loadImageNodes(toSnap.imagePath);
277+ if newerNodes.size > 0 {
278+ newerImg.nodes = newerNodes;
279+ if !quiet then
280+ writeln (" mass-panic diff: loaded " , newerNodes.size,
281+ " nodes from " , toSnap.id);
282+ }
283+ }
284+
266285 const diff = diffSnapshots(olderImg, newerImg, fromSnap.tag, toSnap.tag);
267286
268287 // Output
@@ -301,10 +320,28 @@ module MassPanic {
301320 writeln (" New repos: +" , diff.newNodes.size);
302321 if diff.removedNodes.size > 0 then
303322 writeln (" Gone repos: -" , diff.removedNodes.size);
304- if diff.improvedNodes.size > 0 then
323+
324+ // Per-node breakdown (only populated when full image files are available)
325+ if diff.improvedNodes.size > 0 {
305326 writeln (" Improved: " , diff.improvedNodes.size, " repos" );
306- if diff.degradedNodes.size > 0 then
327+ for delta in diff.improvedNodes {
328+ writeln (" ▲ " , delta.name,
329+ " health " , formatDelta(delta.healthAfter - delta.healthBefore),
330+ " wp " , formatDeltaInt(delta.weakPointsAfter - delta.weakPointsBefore));
331+ }
332+ }
333+ if diff.degradedNodes.size > 0 {
307334 writeln (" Degraded: " , diff.degradedNodes.size, " repos" );
335+ for delta in diff.degradedNodes {
336+ writeln (" ▼ " , delta.name,
337+ " health " , formatDelta(delta.healthAfter - delta.healthBefore),
338+ " wp " , formatDeltaInt(delta.weakPointsAfter - delta.weakPointsBefore));
339+ }
340+ }
341+ if diff.improvedNodes.size == 0 && diff.degradedNodes.size == 0 &&
342+ diff.unchangedCount == 0 {
343+ writeln (" (run two scans to enable per-repo breakdown)" );
344+ }
308345 writeln ();
309346 }
310347
@@ -323,7 +360,31 @@ module MassPanic {
323360 writer.writeln (" \" removed_repos\" : " , diff.removedNodes.size, " ," );
324361 writer.writeln (" \" improved_repos\" : " , diff.improvedNodes.size, " ," );
325362 writer.writeln (" \" degraded_repos\" : " , diff.degradedNodes.size, " ," );
326- writer.writeln (" \" unchanged_repos\" : " , diff.unchangedCount);
363+ writer.writeln (" \" unchanged_repos\" : " , diff.unchangedCount, " ," );
364+
365+ // Per-node deltas — empty arrays when image files were not available
366+ writer.writeln (" \" improved\" : [" );
367+ for (delta, idx) in zip (diff.improvedNodes, 0 ..) {
368+ if idx > 0 then writer.write (" , " );
369+ writer.write (" {\" id\" : \" " , delta.nodeId, " \" , \" name\" : \" " , delta.name, " \" , " );
370+ writer.write (" \" health_before\" : " , delta.healthBefore, " , " );
371+ writer.write (" \" health_after\" : " , delta.healthAfter, " , " );
372+ writer.write (" \" wp_before\" : " , delta.weakPointsBefore, " , " );
373+ writer.write (" \" wp_after\" : " , delta.weakPointsAfter, " }" );
374+ }
375+ writer.writeln (" \n ]," );
376+
377+ writer.writeln (" \" degraded\" : [" );
378+ for (delta, idx) in zip (diff.degradedNodes, 0 ..) {
379+ if idx > 0 then writer.write (" , " );
380+ writer.write (" {\" id\" : \" " , delta.nodeId, " \" , \" name\" : \" " , delta.name, " \" , " );
381+ writer.write (" \" health_before\" : " , delta.healthBefore, " , " );
382+ writer.write (" \" health_after\" : " , delta.healthAfter, " , " );
383+ writer.write (" \" wp_before\" : " , delta.weakPointsBefore, " , " );
384+ writer.write (" \" wp_after\" : " , delta.weakPointsAfter, " }" );
385+ }
386+ writer.writeln (" \n ]" );
387+
327388 writer.writeln (" }" );
328389 }
329390
@@ -489,12 +550,14 @@ module MassPanic {
489550
490551 select mode {
491552 when " assail" {
553+ // --quiet causes assail to emit JSON on stdout (no --output needed)
554+ args.pushBack(" --quiet" );
492555 args.pushBack(" assail" );
493556 args.pushBack(repoPath);
494- args.pushBack(" --output-format=json" );
495557 }
496558 when " assault" {
497559 // Full stress test: assail + attack all axes
560+ args.pushBack(" --quiet" );
498561 args.pushBack(" assault" );
499562 args.pushBack(repoPath);
500563 args.pushBack(" --output-format=json" );
@@ -507,6 +570,7 @@ module MassPanic {
507570 }
508571 when " ambush" {
509572 // Timeline-driven stress test
573+ args.pushBack(" --quiet" );
510574 args.pushBack(" ambush" );
511575 args.pushBack(repoPath);
512576 args.pushBack(" --output-format=json" );
0 commit comments