(projectRoot)
| 316 | return { campaigns, skipped }; |
| 317 | } |
| 318 | |
| 319 | function readOutcomeLedger(projectRoot) { |
| 320 | const completedDir = path.join(projectRoot, '.planning', 'campaigns', 'completed'); |
| 321 | return listFiles(completedDir, (entry) => entry.endsWith('.md')) |
| 322 | .map((filePath) => { |
| 323 | const content = readText(filePath) || ''; |
| 324 | const parsed = parseCampaignContent(content, { slug: path.basename(filePath, '.md') }); |
| 325 | const record = completionRecordSummary(content); |
| 326 | const outcome = record.outcome || |
| 327 | (record.mergeSha ? 'shipped-pr' : '') || |
| 328 | (record.pr ? 'review-package' : '') || |
| 329 | 'archived-completion'; |
| 330 | return { |
| 331 | slug: parsed.slug, |
| 332 | title: parsed.title, |
| 333 | path: relativeProjectPath(projectRoot, filePath), |
| 334 | outcome, |
| 335 | completedAt: record.completedAt, |
| 336 | pr: record.pr, |
| 337 | mergeSha: record.mergeSha, |
| 338 | verification: record.verification, |
| 339 | note: record.note, |
| 340 | modifiedAt: safeMtime(filePath), |
| 341 | }; |
| 342 | }) |
| 343 | .sort((left, right) => { |
| 344 | const leftTime = new Date(left.completedAt || left.modifiedAt || 0).getTime(); |
| 345 | const rightTime = new Date(right.completedAt || right.modifiedAt || 0).getTime(); |
| 346 | return rightTime - leftTime; |
| 347 | }) |
| 348 | .slice(0, 8); |
| 349 | } |
| 350 | |
| 351 | function parseFrontmatterLike(content, key) { |
no test coverage detected