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