formatCapabilityReport function

String formatCapabilityReport(
  1. Map report
)

Format a capability report Map as human-readable text (byte-identical to the legacy proto-report renderer). Built from a line list joined with \n plus a trailing newline — reproducing StringBuffer.writeln semantics — so it self-hosts on the compiled TS/C++/Rust CLIs (which have no StringBuffer).

Implementation

String formatCapabilityReport(Map report) {
  final lines = <String>[];
  lines.add(
    'Ball Capability Audit: ${report['programName']} v${report['programVersion']}',
  );
  lines.add('============================================================');
  lines.add('');

  lines.add('Capabilities:');
  final List capabilities = report['capabilities'];
  for (final entry in capabilities) {
    final icon = entry['riskLevel'] == 'none' ? '✓' : '⚠';
    final List callSites = entry['callSites'];
    final siteCount = callSites.length;
    if (siteCount == 0) {
      lines.add('  $icon ${entry['capability']} (pure computation)');
    } else {
      final siteStrs = <String>[];
      for (final s in callSites) {
        siteStrs.add(
          '${s['module']}.${s['function']} → ${s['calleeModule']}.${s['calleeFunction']}',
        );
      }
      final sites = siteStrs.join(', ');
      lines.add(
        '  $icon ${entry['capability']} ($siteCount call sites: $sites)',
      );
    }
  }

  final Map s = report['summary'];
  final absent = <String>[];
  final readsFs = s['readsFilesystem'];
  final writesFs = s['writesFilesystem'];
  if (readsFs == false && writesFs == false) absent.add('filesystem');
  if (s['usesNetwork'] == false) absent.add('network');
  if (s['controlsProcess'] == false) absent.add('process');
  if (s['usesMemory'] == false) absent.add('memory');
  if (s['usesConcurrency'] == false) absent.add('concurrency');
  if (s['usesRandom'] == false) absent.add('random');
  if (absent.isNotEmpty) {
    lines.add('  ✗ NONE: ${absent.join(', ')}');
  }

  // Base-function shadows (issue #420): a bare-name call to a shadowed name
  // dispatches to the user function, so the shadowed base capability is not
  // itself exercised — but the collision is a review signal (a decoy can hide a
  // capability's name), so it is surfaced explicitly and never as a bare "no
  // risk". `shadows` is always populated by the analyzer; guard defensively for
  // any hand-built report Map (mirrors the `containsKey` idiom used for
  // `capSites` above — engine-safe, no `??`).
  final List shadows = report.containsKey('shadows')
      ? report['shadows']
      : <Object?>[];
  final hasShadows = shadows.isNotEmpty;
  if (hasShadows) {
    lines.add('');
    lines.add('Shadowed base functions:');
    for (final sh in shadows) {
      lines.add(
        '  ⚠ ${sh['module']}.${sh['function']} shadows '
        '${sh['baseModule']}.${sh['function']} '
        '(${sh['capability']}, ${sh['riskLevel']} risk)',
      );
    }
  }

  lines.add('');
  final isPure = s['isPure'] == true;
  final controlsProcess = s['controlsProcess'] == true;
  final usesMemory = s['usesMemory'] == true;
  final usesNetwork = s['usesNetwork'] == true;
  final rFs = s['readsFilesystem'] == true;
  final wFs = s['writesFilesystem'] == true;
  final usesConcurrency = s['usesConcurrency'] == true;
  String risk;
  if (isPure) {
    // A program that only computes yet declares a base-function shadow is not
    // cleanly "no risk" — the bare "pure computation only" line would be
    // misleading, so escalate it to a review prompt (the shadow section above
    // names the offending function and its capability).
    risk = hasShadows
        ? 'REVIEW REQUIRED — declares base-function shadows'
        : 'NO RISK — pure computation only';
  } else if (controlsProcess || usesMemory || usesNetwork) {
    risk = 'HIGH RISK';
  } else if (rFs || wFs || usesConcurrency) {
    risk = 'MEDIUM RISK';
  } else {
    risk = 'LOW RISK';
  }
  lines.add('Summary: $risk');
  lines.add(
    '  ${s['totalFunctions']} functions: ${s['pureFunctions']} pure, ${s['effectfulFunctions']} effectful',
  );

  lines.add('');
  lines.add('Per-function breakdown:');
  final List functions = report['functions'];
  for (final fn in functions) {
    final List fnCaps = fn['capabilities'];
    final nonPure = <String>[];
    for (final c in fnCaps) {
      if (c != 'pure') nonPure.add(c);
    }
    final label = nonPure.isEmpty ? 'pure' : nonPure.join(', ');
    lines.add('  ${fn['module']}.${fn['function']} → $label');
  }

  return '${lines.join('\n')}\n';
}