@anthropologies/claudestory 0.1.2 → 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/README.md +25 -4
- package/dist/cli.js +637 -27
- package/dist/index.d.ts +604 -1
- package/dist/index.js +517 -14
- package/dist/mcp.js +690 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1359,7 +1359,7 @@ function dfsBlocked(id, state, visited, inStack, findings) {
|
|
|
1359
1359
|
}
|
|
1360
1360
|
|
|
1361
1361
|
// src/core/init.ts
|
|
1362
|
-
import { mkdir, stat as stat2
|
|
1362
|
+
import { mkdir, stat as stat2 } from "fs/promises";
|
|
1363
1363
|
import { join as join4, resolve as resolve3 } from "path";
|
|
1364
1364
|
async function initProject(root, options) {
|
|
1365
1365
|
const absRoot = resolve3(root);
|
|
@@ -1418,21 +1418,14 @@ async function initProject(root, options) {
|
|
|
1418
1418
|
await writeRoadmap(roadmap, absRoot);
|
|
1419
1419
|
const warnings = [];
|
|
1420
1420
|
if (options.force && exists) {
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
if (!f.endsWith(".json")) continue;
|
|
1427
|
-
try {
|
|
1428
|
-
const content = await readFile3(join4(dir, f), "utf-8");
|
|
1429
|
-
JSON.parse(content);
|
|
1430
|
-
} catch {
|
|
1431
|
-
warnings.push(`.story/${subdir}/${f}`);
|
|
1432
|
-
}
|
|
1421
|
+
try {
|
|
1422
|
+
const { warnings: loadWarnings } = await loadProject(absRoot);
|
|
1423
|
+
for (const w of loadWarnings) {
|
|
1424
|
+
if (INTEGRITY_WARNING_TYPES.includes(w.type)) {
|
|
1425
|
+
warnings.push(`${w.file}: ${w.message}`);
|
|
1433
1426
|
}
|
|
1434
|
-
} catch {
|
|
1435
1427
|
}
|
|
1428
|
+
} catch {
|
|
1436
1429
|
}
|
|
1437
1430
|
}
|
|
1438
1431
|
return {
|
|
@@ -1448,6 +1441,234 @@ async function initProject(root, options) {
|
|
|
1448
1441
|
};
|
|
1449
1442
|
}
|
|
1450
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
|
+
|
|
1451
1672
|
// src/core/output-formatter.ts
|
|
1452
1673
|
var ExitCode = {
|
|
1453
1674
|
OK: 0,
|
|
@@ -1753,6 +1974,278 @@ function formatHandoverContent(filename, content, format) {
|
|
|
1753
1974
|
}
|
|
1754
1975
|
return content;
|
|
1755
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
|
+
}
|
|
1756
2249
|
function truncate(text, maxLen) {
|
|
1757
2250
|
if (text.length <= maxLen) return text;
|
|
1758
2251
|
return text.slice(0, maxLen - 3) + "...";
|
|
@@ -1782,15 +2275,19 @@ export {
|
|
|
1782
2275
|
ProjectLoaderError,
|
|
1783
2276
|
ProjectState,
|
|
1784
2277
|
RoadmapSchema,
|
|
2278
|
+
SnapshotV1Schema,
|
|
1785
2279
|
TICKET_ID_REGEX,
|
|
1786
2280
|
TICKET_STATUSES,
|
|
1787
2281
|
TICKET_TYPES,
|
|
1788
2282
|
TicketIdSchema,
|
|
1789
2283
|
TicketSchema,
|
|
2284
|
+
atomicWrite,
|
|
1790
2285
|
blockedTickets,
|
|
2286
|
+
buildRecap,
|
|
1791
2287
|
currentPhase,
|
|
1792
2288
|
deleteIssue,
|
|
1793
2289
|
deleteTicket,
|
|
2290
|
+
diffStates,
|
|
1794
2291
|
discoverProjectRoot,
|
|
1795
2292
|
errorEnvelope,
|
|
1796
2293
|
escapeMarkdownInline,
|
|
@@ -1799,6 +2296,7 @@ export {
|
|
|
1799
2296
|
formatBlockedTickets,
|
|
1800
2297
|
formatBlockerList,
|
|
1801
2298
|
formatError,
|
|
2299
|
+
formatExport,
|
|
1802
2300
|
formatHandoverContent,
|
|
1803
2301
|
formatHandoverList,
|
|
1804
2302
|
formatInitResult,
|
|
@@ -1807,13 +2305,17 @@ export {
|
|
|
1807
2305
|
formatNextTicketOutcome,
|
|
1808
2306
|
formatPhaseList,
|
|
1809
2307
|
formatPhaseTickets,
|
|
2308
|
+
formatRecap,
|
|
2309
|
+
formatSnapshotResult,
|
|
1810
2310
|
formatStatus,
|
|
1811
2311
|
formatTicket,
|
|
1812
2312
|
formatTicketList,
|
|
1813
2313
|
formatValidation,
|
|
2314
|
+
guardPath,
|
|
1814
2315
|
initProject,
|
|
1815
2316
|
isBlockerCleared,
|
|
1816
2317
|
listHandovers,
|
|
2318
|
+
loadLatestSnapshot,
|
|
1817
2319
|
loadProject,
|
|
1818
2320
|
mergeValidation,
|
|
1819
2321
|
nextIssueID,
|
|
@@ -1825,6 +2327,7 @@ export {
|
|
|
1825
2327
|
readHandover,
|
|
1826
2328
|
runTransaction,
|
|
1827
2329
|
runTransactionUnlocked,
|
|
2330
|
+
saveSnapshot,
|
|
1828
2331
|
serializeJSON,
|
|
1829
2332
|
sortKeysDeep,
|
|
1830
2333
|
successEnvelope,
|