appendWorldCycleEvidence function

Future<void> appendWorldCycleEvidence({
  1. required String featureDir,
  2. required String behaviorId,
  3. required String kind,
  4. required String commandLine,
  5. required String subjectDigest,
  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 canonical chain — the payload every reader recomputes (CycleLog.chainHashFromFields). kind is world-cert / world-run; subjectDigest is the certified subject's digest (the world hash for certification, the run digest for runs), recorded in its own - digest: field beside the chain link (review #1612, finding 1).

Implementation

Future<void> appendWorldCycleEvidence({
  required String featureDir,
  required String behaviorId,
  required String kind,
  required String commandLine,
  required String subjectDigest,
  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 hash = CycleLog.chainHashFromFields(
    behaviorId: behaviorId,
    kind: kind,
    exit: exitCode.toString(),
    command: commandLine,
    criterion: criterion,
    test: '',
    timestamp: now,
    prevHash: prev,
  );

  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')
    ..writeln('- digest: $subjectDigest');
  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);
}