appendWorldCycleEvidence function

Future<void> appendWorldCycleEvidence({
  1. required String featureDir,
  2. required String behaviorId,
  3. required String kind,
  4. required String commandLine,
  5. required String hash,
  6. required int exitCode,
  7. required String criterion,
  8. Map<String, String> extraLines = const {},
})

Append a hash-chained world evidence entry to the feature's cycle log (<featureDir>/tdd/cycle-log.md), schema-1 chain format — the same format the run driver, doctor, and fixture commitment already parse. kind is world-cert / world-run; hash is the entry's chain hash (the world hash for certification, the run digest for runs).

Implementation

Future<void> appendWorldCycleEvidence({
  required String featureDir,
  required String behaviorId,
  required String kind,
  required String commandLine,
  required String hash,
  required int exitCode,
  required String criterion,
  Map<String, String> extraLines = const {},
}) async {
  final tddDir = Directory(p.join(featureDir, 'tdd'));
  if (!tddDir.existsSync()) tddDir.createSync(recursive: true);
  final file = File(p.join(tddDir.path, 'cycle-log.md'));
  final existing = file.existsSync() ? file.readAsStringSync() : '';
  final cycleEvidence = CycleEvidence(featureDir);
  final prev = await cycleEvidence.lastHashFor(behaviorId) ?? 'genesis';
  final now = DateTime.now().toUtc().toIso8601String();

  final buffer = StringBuffer()
    ..writeln('## $now: $kind (spec 968)')
    ..writeln('- behavior: $behaviorId')
    ..writeln('- kind: $kind')
    ..writeln('- at: $now')
    ..writeln('- exit: $exitCode')
    ..writeln('- criterion: $criterion')
    ..writeln('- command: `$commandLine`')
    ..writeln('- schema: 1')
    ..writeln('- prev-hash: $prev')
    ..writeln('- hash: $hash');
  for (final line in extraLines.entries) {
    buffer.writeln('- ${line.key}: ${line.value}');
  }

  final prefix = existing.isEmpty
      ? '# Cycle log — ${p.basename(p.normalize(featureDir))}\n'
      : (existing.endsWith('\n') ? existing : '$existing\n');
  // Atomic write: write to a temp file then rename to prevent torn reads
  // on concurrent CI runs targeting the same world name.
  final tmp = File('${file.path}.tmp');
  tmp.writeAsStringSync('$prefix\n${buffer.toString()}');
  tmp.renameSync(file.path);
}