Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x 33x 33x 33x 33x 16x 16x 16x 33x 16x 16x 16x 16x 17x 17x 17x 17x 17x 16x 17x 16x 17x 16x 16x 33x 16x 16x 16x 16x 16x 16x 16x 16x 16x 16x | /**
* @module roll-forward-pirs/emitter
* @description Renders the `horizon-pir-rollforward.md` artifact that
* accompanies long-horizon roll-forwards.
*
* @author Hack23 AB
* @license Apache-2.0
*/
import path from 'node:path';
import { CYCLE_HORIZON_DAYS, REPO_ROOT } from './constants.js';
import { addDays } from './date-helpers.js';
import { determineOrigin } from './horizon.js';
import type { PirStatusFile } from './types.js';
/**
* Render a `horizon-pir-rollforward.md` Markdown document from a rolled-forward
* PIR status file. Groups PIRs by status and stamps each open PIR with an
* obsolescence date calculated as `targetDate + horizonDays`.
*
* @param output - The rolled-forward PIR status file to render.
* @param sourcePath - Path of the source file that produced `output`, used to
* compute repo-relative paths in the rendered Markdown.
* @param targetDate - The roll-forward target date (YYYY-MM-DD), anchor for
* the obsolescence calculation.
* @param options - Optional `repoRoot` override and the `sourcePirIds` Set
* identifying which PIRs were inherited from the source file (vs. newly
* created during this run).
* @returns Markdown text for the `horizon-pir-rollforward.md` artifact.
*/
export function emitRollforwardMd(
output: PirStatusFile,
sourcePath: string,
targetDate: string,
options: { repoRoot?: string; sourcePirIds?: Set<string> } = {},
): string {
const repoRoot = options.repoRoot ?? REPO_ROOT;
const horizonDays = CYCLE_HORIZON_DAYS[output.cycle];
let predecessorFolder: string;
if (output.inherited_from) {
predecessorFolder = output.inherited_from.replace(/\/pir-status\.json$/, '');
Iif (predecessorFolder.endsWith('/')) {
predecessorFolder = predecessorFolder.slice(0, -1);
}
} else E{
const relSource = path.relative(repoRoot, sourcePath).split(path.sep).join('/');
predecessorFolder = path.dirname(relSource);
}
const sourceDate = output.inherited_from
? (() => {
const match = /(\d{4}-\d{2}-\d{2})/.exec(predecessorFolder);
return match ? match[1] : 'unknown';
})()
: 'unknown';
const daysSince =
sourceDate !== 'unknown'
? Math.round(
(new Date(`${targetDate}T12:00:00Z`).getTime() -
new Date(`${sourceDate}T12:00:00Z`).getTime()) /
86_400_000,
)
: 0;
const lines: string[] = [];
lines.push(`# 🔁 Horizon PIR Roll-Forward`);
lines.push('');
lines.push(`> Auto-generated by \`scripts/roll-forward-pirs.ts --emit-rollforward-md\``);
lines.push(`> Cycle: **${output.cycle}** | Date: **${targetDate}**`);
lines.push('');
lines.push('---');
lines.push('');
lines.push('## 1 — Predecessor Manifest');
lines.push('');
lines.push('```');
lines.push(`Predecessor folder: ${predecessorFolder}/`);
lines.push(`Days since predecessor: ${daysSince}`);
lines.push('```');
lines.push('');
lines.push('---');
lines.push('');
lines.push('## 2 — PIR Genealogy Table');
lines.push('');
lines.push('| PIR ID | Status | Origin | Confidence | Obsolescence Date | Notes |');
lines.push('|--------|--------|--------|------------|-------------------|-------|');
for (const pir of output.pirs) {
const origin = determineOrigin(pir, options.sourcePirIds, output);
const obsolescenceDate =
pir.status === 'open' ? addDays(targetDate, horizonDays) : '—';
const notes =
pir.status === 'open'
? `Confidence degraded on roll-forward`
: `Status: ${pir.status}`;
lines.push(
`| ${pir.pir_id} | ${pir.status} | ${origin} | ${pir.confidence} | ${obsolescenceDate} | ${notes} |`,
);
}
lines.push('');
lines.push('---');
lines.push('');
const openPirs = output.pirs.filter((p) => p.status === 'open');
lines.push('## 3 — Active PIRs (with obsolescence dates)');
lines.push('');
Iif (openPirs.length === 0) {
lines.push('_No open PIRs carried forward._');
} else {
for (const pir of openPirs) {
lines.push(`### ${pir.pir_id}`);
lines.push(`- **Statement:** ${pir.statement}`);
lines.push(`- **Confidence:** ${pir.confidence}`);
lines.push(`- **Obsolescence date:** ${addDays(targetDate, horizonDays)}`);
if (pir.inherits_from && pir.inherits_from.length > 0) {
lines.push(`- **Inherits from:** ${pir.inherits_from.join(' → ')}`);
}
if (pir.evidence_refs && pir.evidence_refs.length > 0) {
lines.push(`- **Evidence:** ${pir.evidence_refs.join(', ')}`);
}
lines.push('');
}
}
lines.push('---');
lines.push('');
const archivedPirs = output.pirs.filter((p) => p.status !== 'open');
lines.push('## 4 — Archived / Resolved PIRs');
lines.push('');
Iif (archivedPirs.length === 0) {
lines.push('_No archived PIRs in this roll-forward._');
} else {
lines.push('| PIR ID | Status | Confidence | Notes |');
lines.push('|--------|--------|------------|-------|');
for (const pir of archivedPirs) {
const notes = pir.answer_summary ? pir.answer_summary : '—';
lines.push(`| ${pir.pir_id} | ${pir.status} | ${pir.confidence} | ${notes} |`);
}
}
lines.push('');
return lines.join('\n');
}
|