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