@anthropologies/claudestory 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1441,6 +1441,234 @@ async function initProject(root, options) {
1441
1441
  };
1442
1442
  }
1443
1443
 
1444
+ // src/core/snapshot.ts
1445
+ import { readdir as readdir3, readFile as readFile3, mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
1446
+ import { existsSync as existsSync4 } from "fs";
1447
+ import { join as join5, resolve as resolve4 } from "path";
1448
+ import { z as z6 } from "zod";
1449
+ var LoadWarningSchema = z6.object({
1450
+ type: z6.string(),
1451
+ file: z6.string(),
1452
+ message: z6.string()
1453
+ });
1454
+ var SnapshotV1Schema = z6.object({
1455
+ version: z6.literal(1),
1456
+ createdAt: z6.string().datetime({ offset: true }),
1457
+ project: z6.string(),
1458
+ config: ConfigSchema,
1459
+ roadmap: RoadmapSchema,
1460
+ tickets: z6.array(TicketSchema),
1461
+ issues: z6.array(IssueSchema),
1462
+ warnings: z6.array(LoadWarningSchema).optional()
1463
+ });
1464
+ var MAX_SNAPSHOTS = 20;
1465
+ async function saveSnapshot(root, loadResult) {
1466
+ const absRoot = resolve4(root);
1467
+ const snapshotsDir = join5(absRoot, ".story", "snapshots");
1468
+ await mkdir2(snapshotsDir, { recursive: true });
1469
+ const { state, warnings } = loadResult;
1470
+ const now = /* @__PURE__ */ new Date();
1471
+ const filename = formatSnapshotFilename(now);
1472
+ const snapshot = {
1473
+ version: 1,
1474
+ createdAt: now.toISOString(),
1475
+ project: state.config.project,
1476
+ config: state.config,
1477
+ roadmap: state.roadmap,
1478
+ tickets: [...state.tickets],
1479
+ issues: [...state.issues],
1480
+ ...warnings.length > 0 ? {
1481
+ warnings: warnings.map((w) => ({
1482
+ type: w.type,
1483
+ file: w.file,
1484
+ message: w.message
1485
+ }))
1486
+ } : {}
1487
+ };
1488
+ const json = JSON.stringify(snapshot, null, 2) + "\n";
1489
+ const targetPath = join5(snapshotsDir, filename);
1490
+ const wrapDir = join5(absRoot, ".story");
1491
+ await guardPath(targetPath, wrapDir);
1492
+ await atomicWrite(targetPath, json);
1493
+ const pruned = await pruneSnapshots(snapshotsDir);
1494
+ const entries = await listSnapshotFiles(snapshotsDir);
1495
+ return { filename, retained: entries.length, pruned };
1496
+ }
1497
+ async function loadLatestSnapshot(root) {
1498
+ const snapshotsDir = join5(resolve4(root), ".story", "snapshots");
1499
+ if (!existsSync4(snapshotsDir)) return null;
1500
+ const files = await listSnapshotFiles(snapshotsDir);
1501
+ if (files.length === 0) return null;
1502
+ for (const filename of files) {
1503
+ try {
1504
+ const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
1505
+ const parsed = JSON.parse(content);
1506
+ const snapshot = SnapshotV1Schema.parse(parsed);
1507
+ return { snapshot, filename };
1508
+ } catch {
1509
+ continue;
1510
+ }
1511
+ }
1512
+ return null;
1513
+ }
1514
+ function diffStates(snapshotState, currentState) {
1515
+ const snapTickets = new Map(snapshotState.tickets.map((t) => [t.id, t]));
1516
+ const curTickets = new Map(currentState.tickets.map((t) => [t.id, t]));
1517
+ const ticketsAdded = [];
1518
+ const ticketsRemoved = [];
1519
+ const ticketsStatusChanged = [];
1520
+ for (const [id, cur] of curTickets) {
1521
+ const snap = snapTickets.get(id);
1522
+ if (!snap) {
1523
+ ticketsAdded.push({ id, title: cur.title });
1524
+ } else if (snap.status !== cur.status) {
1525
+ ticketsStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
1526
+ }
1527
+ }
1528
+ for (const [id, snap] of snapTickets) {
1529
+ if (!curTickets.has(id)) {
1530
+ ticketsRemoved.push({ id, title: snap.title });
1531
+ }
1532
+ }
1533
+ const snapIssues = new Map(snapshotState.issues.map((i) => [i.id, i]));
1534
+ const curIssues = new Map(currentState.issues.map((i) => [i.id, i]));
1535
+ const issuesAdded = [];
1536
+ const issuesResolved = [];
1537
+ const issuesStatusChanged = [];
1538
+ for (const [id, cur] of curIssues) {
1539
+ const snap = snapIssues.get(id);
1540
+ if (!snap) {
1541
+ issuesAdded.push({ id, title: cur.title });
1542
+ } else if (snap.status !== cur.status) {
1543
+ if (cur.status === "resolved") {
1544
+ issuesResolved.push({ id, title: cur.title });
1545
+ } else {
1546
+ issuesStatusChanged.push({ id, title: cur.title, from: snap.status, to: cur.status });
1547
+ }
1548
+ }
1549
+ }
1550
+ const snapBlockers = new Map(
1551
+ snapshotState.roadmap.blockers.map((b) => [b.name, b])
1552
+ );
1553
+ const curBlockers = new Map(
1554
+ currentState.roadmap.blockers.map((b) => [b.name, b])
1555
+ );
1556
+ const blockersAdded = [];
1557
+ const blockersCleared = [];
1558
+ for (const [name, cur] of curBlockers) {
1559
+ const snap = snapBlockers.get(name);
1560
+ if (!snap) {
1561
+ blockersAdded.push(name);
1562
+ } else if (!isBlockerCleared(snap) && isBlockerCleared(cur)) {
1563
+ blockersCleared.push(name);
1564
+ }
1565
+ }
1566
+ const snapPhases = snapshotState.roadmap.phases;
1567
+ const curPhases = currentState.roadmap.phases;
1568
+ const snapPhaseMap = new Map(snapPhases.map((p) => [p.id, p]));
1569
+ const curPhaseMap = new Map(curPhases.map((p) => [p.id, p]));
1570
+ const phasesAdded = [];
1571
+ const phasesRemoved = [];
1572
+ const phasesStatusChanged = [];
1573
+ for (const [id, curPhase] of curPhaseMap) {
1574
+ const snapPhase = snapPhaseMap.get(id);
1575
+ if (!snapPhase) {
1576
+ phasesAdded.push({ id, name: curPhase.name });
1577
+ } else {
1578
+ const snapStatus = snapshotState.phaseStatus(id);
1579
+ const curStatus = currentState.phaseStatus(id);
1580
+ if (snapStatus !== curStatus) {
1581
+ phasesStatusChanged.push({
1582
+ id,
1583
+ name: curPhase.name,
1584
+ from: snapStatus,
1585
+ to: curStatus
1586
+ });
1587
+ }
1588
+ }
1589
+ }
1590
+ for (const [id, snapPhase] of snapPhaseMap) {
1591
+ if (!curPhaseMap.has(id)) {
1592
+ phasesRemoved.push({ id, name: snapPhase.name });
1593
+ }
1594
+ }
1595
+ return {
1596
+ tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged },
1597
+ issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged },
1598
+ blockers: { added: blockersAdded, cleared: blockersCleared },
1599
+ phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged }
1600
+ };
1601
+ }
1602
+ function buildRecap(currentState, snapshotInfo) {
1603
+ const next = nextTicket(currentState);
1604
+ const nextTicketAction = next.kind === "found" ? { id: next.ticket.id, title: next.ticket.title, phase: next.ticket.phase } : null;
1605
+ const highSeverityIssues = currentState.issues.filter(
1606
+ (i) => i.status !== "resolved" && (i.severity === "critical" || i.severity === "high")
1607
+ ).map((i) => ({ id: i.id, title: i.title, severity: i.severity }));
1608
+ if (!snapshotInfo) {
1609
+ return {
1610
+ snapshot: null,
1611
+ changes: null,
1612
+ suggestedActions: {
1613
+ nextTicket: nextTicketAction,
1614
+ highSeverityIssues,
1615
+ recentlyClearedBlockers: []
1616
+ },
1617
+ partial: false
1618
+ };
1619
+ }
1620
+ const { snapshot, filename } = snapshotInfo;
1621
+ const snapshotState = new ProjectState({
1622
+ tickets: snapshot.tickets,
1623
+ issues: snapshot.issues,
1624
+ roadmap: snapshot.roadmap,
1625
+ config: snapshot.config,
1626
+ handoverFilenames: []
1627
+ });
1628
+ const changes = diffStates(snapshotState, currentState);
1629
+ const recentlyClearedBlockers = changes.blockers.cleared;
1630
+ return {
1631
+ snapshot: { filename, createdAt: snapshot.createdAt },
1632
+ changes,
1633
+ suggestedActions: {
1634
+ nextTicket: nextTicketAction,
1635
+ highSeverityIssues,
1636
+ recentlyClearedBlockers
1637
+ },
1638
+ partial: (snapshot.warnings ?? []).length > 0
1639
+ };
1640
+ }
1641
+ function formatSnapshotFilename(date) {
1642
+ const y = date.getUTCFullYear();
1643
+ const mo = String(date.getUTCMonth() + 1).padStart(2, "0");
1644
+ const d = String(date.getUTCDate()).padStart(2, "0");
1645
+ const h = String(date.getUTCHours()).padStart(2, "0");
1646
+ const mi = String(date.getUTCMinutes()).padStart(2, "0");
1647
+ const s = String(date.getUTCSeconds()).padStart(2, "0");
1648
+ const ms = String(date.getUTCMilliseconds()).padStart(3, "0");
1649
+ return `${y}-${mo}-${d}T${h}-${mi}-${s}-${ms}.json`;
1650
+ }
1651
+ async function listSnapshotFiles(dir) {
1652
+ try {
1653
+ const entries = await readdir3(dir);
1654
+ return entries.filter((f) => f.endsWith(".json") && !f.startsWith(".")).sort().reverse();
1655
+ } catch {
1656
+ return [];
1657
+ }
1658
+ }
1659
+ async function pruneSnapshots(dir) {
1660
+ const files = await listSnapshotFiles(dir);
1661
+ if (files.length <= MAX_SNAPSHOTS) return 0;
1662
+ const toRemove = files.slice(MAX_SNAPSHOTS);
1663
+ for (const f of toRemove) {
1664
+ try {
1665
+ await unlink2(join5(dir, f));
1666
+ } catch {
1667
+ }
1668
+ }
1669
+ return toRemove.length;
1670
+ }
1671
+
1444
1672
  // src/core/output-formatter.ts
1445
1673
  var ExitCode = {
1446
1674
  OK: 0,
@@ -1746,6 +1974,278 @@ function formatHandoverContent(filename, content, format) {
1746
1974
  }
1747
1975
  return content;
1748
1976
  }
1977
+ function formatSnapshotResult(result, format) {
1978
+ if (format === "json") {
1979
+ return JSON.stringify(successEnvelope(result), null, 2);
1980
+ }
1981
+ let line = `Snapshot saved: ${result.filename} (${result.retained} retained`;
1982
+ if (result.pruned > 0) line += `, ${result.pruned} pruned`;
1983
+ line += ")";
1984
+ return line;
1985
+ }
1986
+ function formatRecap(recap, state, format) {
1987
+ if (format === "json") {
1988
+ return JSON.stringify(successEnvelope(recap), null, 2);
1989
+ }
1990
+ const lines = [];
1991
+ if (!recap.snapshot) {
1992
+ lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
1993
+ lines.push("");
1994
+ lines.push("No snapshot found. Run `claudestory snapshot` to enable session diffs.");
1995
+ lines.push("");
1996
+ lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete, ${state.blockedCount} blocked`);
1997
+ lines.push(`Issues: ${state.openIssueCount} open`);
1998
+ } else {
1999
+ lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Recap`);
2000
+ lines.push("");
2001
+ lines.push(`Since snapshot: ${recap.snapshot.createdAt}`);
2002
+ if (recap.partial) {
2003
+ lines.push("**Note:** Snapshot was taken from a project with integrity warnings. Diff may be incomplete.");
2004
+ }
2005
+ const changes = recap.changes;
2006
+ const hasChanges = hasAnyChanges(changes);
2007
+ if (!hasChanges) {
2008
+ lines.push("");
2009
+ lines.push("No changes since last snapshot.");
2010
+ } else {
2011
+ if (changes.phases.statusChanged.length > 0) {
2012
+ lines.push("");
2013
+ lines.push("## Phase Transitions");
2014
+ for (const p of changes.phases.statusChanged) {
2015
+ lines.push(`- **${escapeMarkdownInline(p.name)}** (${p.id}): ${p.from} \u2192 ${p.to}`);
2016
+ }
2017
+ }
2018
+ const ticketChanges = changes.tickets;
2019
+ if (ticketChanges.added.length > 0 || ticketChanges.removed.length > 0 || ticketChanges.statusChanged.length > 0) {
2020
+ lines.push("");
2021
+ lines.push("## Tickets");
2022
+ for (const t of ticketChanges.statusChanged) {
2023
+ lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 ${t.from} \u2192 ${t.to}`);
2024
+ }
2025
+ for (const t of ticketChanges.added) {
2026
+ lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **new**`);
2027
+ }
2028
+ for (const t of ticketChanges.removed) {
2029
+ lines.push(`- ${t.id}: ${escapeMarkdownInline(t.title)} \u2014 **removed**`);
2030
+ }
2031
+ }
2032
+ const issueChanges = changes.issues;
2033
+ if (issueChanges.added.length > 0 || issueChanges.resolved.length > 0 || issueChanges.statusChanged.length > 0) {
2034
+ lines.push("");
2035
+ lines.push("## Issues");
2036
+ for (const i of issueChanges.resolved) {
2037
+ lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **resolved**`);
2038
+ }
2039
+ for (const i of issueChanges.statusChanged) {
2040
+ lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 ${i.from} \u2192 ${i.to}`);
2041
+ }
2042
+ for (const i of issueChanges.added) {
2043
+ lines.push(`- ${i.id}: ${escapeMarkdownInline(i.title)} \u2014 **new**`);
2044
+ }
2045
+ }
2046
+ if (changes.blockers.added.length > 0 || changes.blockers.cleared.length > 0) {
2047
+ lines.push("");
2048
+ lines.push("## Blockers");
2049
+ for (const name of changes.blockers.cleared) {
2050
+ lines.push(`- ${escapeMarkdownInline(name)} \u2014 **cleared**`);
2051
+ }
2052
+ for (const name of changes.blockers.added) {
2053
+ lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
2054
+ }
2055
+ }
2056
+ }
2057
+ }
2058
+ const actions = recap.suggestedActions;
2059
+ lines.push("");
2060
+ lines.push("## Suggested Actions");
2061
+ if (actions.nextTicket) {
2062
+ lines.push(`- **Next:** ${actions.nextTicket.id} \u2014 ${escapeMarkdownInline(actions.nextTicket.title)}${actions.nextTicket.phase ? ` (${actions.nextTicket.phase})` : ""}`);
2063
+ }
2064
+ if (actions.highSeverityIssues.length > 0) {
2065
+ for (const i of actions.highSeverityIssues) {
2066
+ lines.push(`- **${i.severity} issue:** ${i.id} \u2014 ${escapeMarkdownInline(i.title)}`);
2067
+ }
2068
+ }
2069
+ if (actions.recentlyClearedBlockers.length > 0) {
2070
+ lines.push(`- **Recently cleared:** ${actions.recentlyClearedBlockers.map(escapeMarkdownInline).join(", ")}`);
2071
+ }
2072
+ if (!actions.nextTicket && actions.highSeverityIssues.length === 0 && actions.recentlyClearedBlockers.length === 0) {
2073
+ lines.push("- No urgent actions.");
2074
+ }
2075
+ return lines.join("\n");
2076
+ }
2077
+ function formatExport(state, mode, phaseId, format) {
2078
+ if (mode === "phase" && phaseId) {
2079
+ return formatPhaseExport(state, phaseId, format);
2080
+ }
2081
+ return formatFullExport(state, format);
2082
+ }
2083
+ function formatPhaseExport(state, phaseId, format) {
2084
+ const phase = state.roadmap.phases.find((p) => p.id === phaseId);
2085
+ if (!phase) {
2086
+ return formatError("not_found", `Phase "${phaseId}" not found`, format);
2087
+ }
2088
+ const phaseStatus = state.phaseStatus(phaseId);
2089
+ const leaves = state.phaseTickets(phaseId);
2090
+ const umbrellaAncestors = /* @__PURE__ */ new Map();
2091
+ for (const leaf of leaves) {
2092
+ if (leaf.parentTicket) {
2093
+ const parent = state.ticketByID(leaf.parentTicket);
2094
+ if (parent && !umbrellaAncestors.has(parent.id)) {
2095
+ umbrellaAncestors.set(parent.id, parent);
2096
+ }
2097
+ }
2098
+ }
2099
+ const crossPhaseDeps = /* @__PURE__ */ new Map();
2100
+ for (const leaf of leaves) {
2101
+ for (const blockerId of leaf.blockedBy) {
2102
+ const blocker = state.ticketByID(blockerId);
2103
+ if (blocker && blocker.phase !== phaseId && !crossPhaseDeps.has(blocker.id)) {
2104
+ crossPhaseDeps.set(blocker.id, blocker);
2105
+ }
2106
+ }
2107
+ }
2108
+ const relatedIssues = state.issues.filter(
2109
+ (i) => i.status !== "resolved" && (i.phase === phaseId || i.relatedTickets.some((tid) => {
2110
+ const t = state.ticketByID(tid);
2111
+ return t && t.phase === phaseId;
2112
+ }))
2113
+ );
2114
+ const activeBlockers = state.roadmap.blockers.filter(
2115
+ (b) => !isBlockerCleared(b)
2116
+ );
2117
+ if (format === "json") {
2118
+ return JSON.stringify(
2119
+ successEnvelope({
2120
+ phase: { id: phase.id, name: phase.name, description: phase.description, status: phaseStatus },
2121
+ tickets: leaves.map((t) => ({ id: t.id, title: t.title, status: t.status, type: t.type, order: t.order })),
2122
+ umbrellaAncestors: [...umbrellaAncestors.values()].map((t) => ({ id: t.id, title: t.title })),
2123
+ crossPhaseDependencies: [...crossPhaseDeps.values()].map((t) => ({ id: t.id, title: t.title, status: t.status, phase: t.phase })),
2124
+ issues: relatedIssues.map((i) => ({ id: i.id, title: i.title, severity: i.severity, status: i.status })),
2125
+ blockers: activeBlockers.map((b) => ({ name: b.name, note: b.note ?? null }))
2126
+ }),
2127
+ null,
2128
+ 2
2129
+ );
2130
+ }
2131
+ const lines = [];
2132
+ lines.push(`# ${escapeMarkdownInline(phase.name)} (${phase.id})`);
2133
+ lines.push("");
2134
+ lines.push(`Status: ${phaseStatus}`);
2135
+ if (phase.description) {
2136
+ lines.push(`Description: ${escapeMarkdownInline(phase.description)}`);
2137
+ }
2138
+ if (leaves.length > 0) {
2139
+ lines.push("");
2140
+ lines.push("## Tickets");
2141
+ for (const t of leaves) {
2142
+ const indicator = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
2143
+ const parentNote = t.parentTicket && umbrellaAncestors.has(t.parentTicket) ? ` (under ${t.parentTicket})` : "";
2144
+ lines.push(`${indicator} ${t.id}: ${escapeMarkdownInline(t.title)}${parentNote}`);
2145
+ }
2146
+ }
2147
+ if (crossPhaseDeps.size > 0) {
2148
+ lines.push("");
2149
+ lines.push("## Cross-Phase Dependencies");
2150
+ for (const [, dep] of crossPhaseDeps) {
2151
+ lines.push(`- ${dep.id}: ${escapeMarkdownInline(dep.title)} [${dep.status}] (${dep.phase ?? "unphased"})`);
2152
+ }
2153
+ }
2154
+ if (relatedIssues.length > 0) {
2155
+ lines.push("");
2156
+ lines.push("## Open Issues");
2157
+ for (const i of relatedIssues) {
2158
+ lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}`);
2159
+ }
2160
+ }
2161
+ if (activeBlockers.length > 0) {
2162
+ lines.push("");
2163
+ lines.push("## Active Blockers");
2164
+ for (const b of activeBlockers) {
2165
+ lines.push(`- ${escapeMarkdownInline(b.name)}${b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : ""}`);
2166
+ }
2167
+ }
2168
+ return lines.join("\n");
2169
+ }
2170
+ function formatFullExport(state, format) {
2171
+ const phases = phasesWithStatus(state);
2172
+ if (format === "json") {
2173
+ return JSON.stringify(
2174
+ successEnvelope({
2175
+ project: state.config.project,
2176
+ phases: phases.map((p) => ({
2177
+ id: p.phase.id,
2178
+ name: p.phase.name,
2179
+ description: p.phase.description,
2180
+ status: p.status,
2181
+ tickets: state.phaseTickets(p.phase.id).map((t) => ({
2182
+ id: t.id,
2183
+ title: t.title,
2184
+ status: t.status,
2185
+ type: t.type
2186
+ }))
2187
+ })),
2188
+ issues: state.issues.map((i) => ({
2189
+ id: i.id,
2190
+ title: i.title,
2191
+ severity: i.severity,
2192
+ status: i.status
2193
+ })),
2194
+ blockers: state.roadmap.blockers.map((b) => ({
2195
+ name: b.name,
2196
+ cleared: isBlockerCleared(b),
2197
+ note: b.note ?? null
2198
+ }))
2199
+ }),
2200
+ null,
2201
+ 2
2202
+ );
2203
+ }
2204
+ const lines = [];
2205
+ lines.push(`# ${escapeMarkdownInline(state.config.project)} \u2014 Full Export`);
2206
+ lines.push("");
2207
+ lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
2208
+ lines.push(`Issues: ${state.openIssueCount} open`);
2209
+ lines.push("");
2210
+ lines.push("## Phases");
2211
+ for (const p of phases) {
2212
+ const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
2213
+ lines.push("");
2214
+ lines.push(`### ${indicator} ${escapeMarkdownInline(p.phase.name)} (${p.phase.id})`);
2215
+ if (p.phase.description) {
2216
+ lines.push(escapeMarkdownInline(p.phase.description));
2217
+ }
2218
+ const tickets = state.phaseTickets(p.phase.id);
2219
+ if (tickets.length > 0) {
2220
+ lines.push("");
2221
+ for (const t of tickets) {
2222
+ const ti = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
2223
+ lines.push(`${ti} ${t.id}: ${escapeMarkdownInline(t.title)}`);
2224
+ }
2225
+ }
2226
+ }
2227
+ if (state.issues.length > 0) {
2228
+ lines.push("");
2229
+ lines.push("## Issues");
2230
+ for (const i of state.issues) {
2231
+ const resolved = i.status === "resolved" ? " \u2713" : "";
2232
+ lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownInline(i.title)}${resolved}`);
2233
+ }
2234
+ }
2235
+ const blockers = state.roadmap.blockers;
2236
+ if (blockers.length > 0) {
2237
+ lines.push("");
2238
+ lines.push("## Blockers");
2239
+ for (const b of blockers) {
2240
+ const cleared = isBlockerCleared(b) ? "[x]" : "[ ]";
2241
+ lines.push(`${cleared} ${escapeMarkdownInline(b.name)}${b.note ? ` \u2014 ${escapeMarkdownInline(b.note)}` : ""}`);
2242
+ }
2243
+ }
2244
+ return lines.join("\n");
2245
+ }
2246
+ function hasAnyChanges(diff) {
2247
+ return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0;
2248
+ }
1749
2249
  function truncate(text, maxLen) {
1750
2250
  if (text.length <= maxLen) return text;
1751
2251
  return text.slice(0, maxLen - 3) + "...";
@@ -1775,15 +2275,19 @@ export {
1775
2275
  ProjectLoaderError,
1776
2276
  ProjectState,
1777
2277
  RoadmapSchema,
2278
+ SnapshotV1Schema,
1778
2279
  TICKET_ID_REGEX,
1779
2280
  TICKET_STATUSES,
1780
2281
  TICKET_TYPES,
1781
2282
  TicketIdSchema,
1782
2283
  TicketSchema,
2284
+ atomicWrite,
1783
2285
  blockedTickets,
2286
+ buildRecap,
1784
2287
  currentPhase,
1785
2288
  deleteIssue,
1786
2289
  deleteTicket,
2290
+ diffStates,
1787
2291
  discoverProjectRoot,
1788
2292
  errorEnvelope,
1789
2293
  escapeMarkdownInline,
@@ -1792,6 +2296,7 @@ export {
1792
2296
  formatBlockedTickets,
1793
2297
  formatBlockerList,
1794
2298
  formatError,
2299
+ formatExport,
1795
2300
  formatHandoverContent,
1796
2301
  formatHandoverList,
1797
2302
  formatInitResult,
@@ -1800,13 +2305,17 @@ export {
1800
2305
  formatNextTicketOutcome,
1801
2306
  formatPhaseList,
1802
2307
  formatPhaseTickets,
2308
+ formatRecap,
2309
+ formatSnapshotResult,
1803
2310
  formatStatus,
1804
2311
  formatTicket,
1805
2312
  formatTicketList,
1806
2313
  formatValidation,
2314
+ guardPath,
1807
2315
  initProject,
1808
2316
  isBlockerCleared,
1809
2317
  listHandovers,
2318
+ loadLatestSnapshot,
1810
2319
  loadProject,
1811
2320
  mergeValidation,
1812
2321
  nextIssueID,
@@ -1818,6 +2327,7 @@ export {
1818
2327
  readHandover,
1819
2328
  runTransaction,
1820
2329
  runTransactionUnlocked,
2330
+ saveSnapshot,
1821
2331
  serializeJSON,
1822
2332
  sortKeysDeep,
1823
2333
  successEnvelope,