appendWorldCycleEvidence function
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);
}