@smithers-orchestrator/time-travel 0.16.0

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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +68 -0
  3. package/src/BranchInfo.ts +11 -0
  4. package/src/ForkParams.ts +11 -0
  5. package/src/JUMP_MAX_FRAME_NO.js +1 -0
  6. package/src/JUMP_RUN_ID_PATTERN.js +1 -0
  7. package/src/JumpResult.ts +9 -0
  8. package/src/JumpStepName.ts +10 -0
  9. package/src/JumpToFrameError.js +23 -0
  10. package/src/JumpToFrameInput.ts +36 -0
  11. package/src/NodeChange.ts +7 -0
  12. package/src/NodeSnapshot.ts +8 -0
  13. package/src/OutputChange.ts +5 -0
  14. package/src/ParsedSnapshot.ts +18 -0
  15. package/src/REWIND_RATE_LIMIT_MAX.js +1 -0
  16. package/src/REWIND_RATE_LIMIT_WINDOW_MS.js +1 -0
  17. package/src/RalphChange.ts +7 -0
  18. package/src/RalphSnapshot.ts +5 -0
  19. package/src/ReplayParams.ts +12 -0
  20. package/src/ReplayResult.ts +11 -0
  21. package/src/RetryTaskOptions.ts +10 -0
  22. package/src/RetryTaskResult.ts +5 -0
  23. package/src/RevertOptions.ts +9 -0
  24. package/src/RevertResult.ts +5 -0
  25. package/src/RewindAuditResult.ts +5 -0
  26. package/src/RewindLockHandle.ts +4 -0
  27. package/src/RunTimeline.ts +11 -0
  28. package/src/SnapshotDiff.ts +18 -0
  29. package/src/TimeTravelOptions.ts +11 -0
  30. package/src/TimeTravelResult.ts +7 -0
  31. package/src/TimelineFrame.ts +11 -0
  32. package/src/TimelineTree.ts +9 -0
  33. package/src/acquireRewindLock.js +32 -0
  34. package/src/countRecentRewindAuditRows.js +27 -0
  35. package/src/diff.js +189 -0
  36. package/src/evaluateRewindRateLimit.js +41 -0
  37. package/src/fork/_helpers.js +28 -0
  38. package/src/fork/forkRunEffect.js +147 -0
  39. package/src/fork/getBranchInfoEffect.js +26 -0
  40. package/src/fork/index.js +41 -0
  41. package/src/fork/listBranchesEffect.js +25 -0
  42. package/src/hasRewindLock.js +11 -0
  43. package/src/index.d.ts +1170 -0
  44. package/src/index.js +43 -0
  45. package/src/jumpToFrame.js +1077 -0
  46. package/src/listRewindAuditRows.js +83 -0
  47. package/src/metrics.js +4 -0
  48. package/src/recoverInProgressRewindAudits.js +72 -0
  49. package/src/replay.js +22 -0
  50. package/src/replayFromCheckpointEffect.js +59 -0
  51. package/src/replaysStarted.js +2 -0
  52. package/src/resetRewindLocksForTests.js +8 -0
  53. package/src/resolveRewindAuditClient.js +38 -0
  54. package/src/retry-task.js +215 -0
  55. package/src/revert.js +68 -0
  56. package/src/rewindAudit.js +9 -0
  57. package/src/rewindLock.js +7 -0
  58. package/src/rewindLockStore.js +8 -0
  59. package/src/rewindRateLimit.js +3 -0
  60. package/src/runForksCreated.js +2 -0
  61. package/src/schema.js +46 -0
  62. package/src/snapshot/Snapshot.ts +15 -0
  63. package/src/snapshot/SnapshotData.ts +19 -0
  64. package/src/snapshot/captureSnapshotEffect.js +70 -0
  65. package/src/snapshot/index.js +57 -0
  66. package/src/snapshot/listSnapshotsEffect.js +32 -0
  67. package/src/snapshot/loadSnapshotEffect.js +46 -0
  68. package/src/snapshot/parseSnapshot.js +31 -0
  69. package/src/snapshotDuration.js +7 -0
  70. package/src/snapshotsCaptured.js +2 -0
  71. package/src/timeline/_helpers.js +7 -0
  72. package/src/timeline/buildTimelineEffect.js +38 -0
  73. package/src/timeline/buildTimelineTreeEffect.js +30 -0
  74. package/src/timeline/formatTimelineAsJson.js +23 -0
  75. package/src/timeline/formatTimelineForTui.js +31 -0
  76. package/src/timeline/index.js +31 -0
  77. package/src/timetravel.js +247 -0
  78. package/src/types.ts +15 -0
  79. package/src/updateRewindAuditRow.js +35 -0
  80. package/src/validateJumpFrameNo.js +23 -0
  81. package/src/validateJumpRunId.js +18 -0
  82. package/src/vcs-version/VcsTag.ts +9 -0
  83. package/src/vcs-version/index.js +61 -0
  84. package/src/vcs-version/loadVcsTagEffect.js +27 -0
  85. package/src/vcs-version/rerunAtRevisionEffect.js +25 -0
  86. package/src/vcs-version/resolveWorkflowAtRevisionEffect.js +32 -0
  87. package/src/vcs-version/tagSnapshotVcsEffect.js +56 -0
  88. package/src/writeRewindAuditRow.js +46 -0
@@ -0,0 +1,19 @@
1
+ export type SnapshotData = {
2
+ nodes: Array<{
3
+ nodeId: string;
4
+ iteration: number;
5
+ state: string;
6
+ lastAttempt: number | null;
7
+ outputTable: string;
8
+ label: string | null;
9
+ }>;
10
+ outputs: Record<string, unknown>;
11
+ ralph: Array<{
12
+ ralphId: string;
13
+ iteration: number;
14
+ done: boolean;
15
+ }>;
16
+ input: Record<string, unknown>;
17
+ vcsPointer?: string | null;
18
+ workflowHash?: string | null;
19
+ };
@@ -0,0 +1,70 @@
1
+ import { Effect, Metric } from "effect";
2
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
3
+ import { createHash } from "node:crypto";
4
+ import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
5
+ import { smithersSnapshots } from "../schema.js";
6
+ import { snapshotsCaptured } from "../snapshotsCaptured.js";
7
+ import { snapshotDuration } from "../snapshotDuration.js";
8
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
9
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
10
+ /** @typedef {import("./Snapshot.ts").Snapshot} Snapshot */
11
+ /** @typedef {import("./SnapshotData.ts").SnapshotData} SnapshotData */
12
+
13
+ /**
14
+ * @param {SnapshotData} data
15
+ * @returns {string}
16
+ */
17
+ function serializeSnapshotContent(data) {
18
+ return JSON.stringify({
19
+ nodes: data.nodes,
20
+ outputs: data.outputs,
21
+ ralph: data.ralph,
22
+ input: data.input,
23
+ });
24
+ }
25
+ /**
26
+ * @param {SmithersDb} adapter
27
+ * @param {string} runId
28
+ * @param {number} frameNo
29
+ * @param {SnapshotData} data
30
+ * @returns {Effect.Effect<Snapshot, SmithersError>}
31
+ */
32
+ export function captureSnapshot(adapter, runId, frameNo, data) {
33
+ return Effect.gen(function* () {
34
+ const start = performance.now();
35
+ const nodesJson = JSON.stringify(data.nodes);
36
+ const outputsJson = JSON.stringify(data.outputs);
37
+ const ralphJson = JSON.stringify(data.ralph);
38
+ const inputJson = JSON.stringify(data.input);
39
+ const contentHash = createHash("sha256").update(serializeSnapshotContent(data)).digest("hex");
40
+ const ts = nowMs();
41
+ const row = {
42
+ runId,
43
+ frameNo,
44
+ nodesJson,
45
+ outputsJson,
46
+ ralphJson,
47
+ inputJson,
48
+ vcsPointer: data.vcsPointer ?? null,
49
+ workflowHash: data.workflowHash ?? null,
50
+ contentHash,
51
+ createdAtMs: ts,
52
+ };
53
+ yield* Effect.tryPromise({
54
+ try: () => adapter.db
55
+ .insert(smithersSnapshots)
56
+ .values(row)
57
+ .onConflictDoUpdate({
58
+ target: [smithersSnapshots.runId, smithersSnapshots.frameNo],
59
+ set: row,
60
+ }),
61
+ catch: (cause) => toSmithersError(cause, "insert snapshot", {
62
+ code: "DB_WRITE_FAILED",
63
+ details: { frameNo, runId },
64
+ }),
65
+ });
66
+ yield* Metric.increment(snapshotsCaptured);
67
+ yield* Metric.update(snapshotDuration, performance.now() - start);
68
+ return row;
69
+ }).pipe(Effect.annotateLogs({ runId, frameNo: String(frameNo) }), Effect.withLogSpan("time-travel:capture-snapshot"));
70
+ }
@@ -0,0 +1,57 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./Snapshot.ts").Snapshot} Snapshot */
3
+ /** @typedef {import("./SnapshotData.ts").SnapshotData} SnapshotData */
4
+ // @smithers-type-exports-end
5
+
6
+ import { Effect } from "effect";
7
+ import { captureSnapshot as captureSnapshotEffect } from "./captureSnapshotEffect.js";
8
+ import { loadLatestSnapshot as loadLatestSnapshotEffect, loadSnapshot as loadSnapshotEffect, } from "./loadSnapshotEffect.js";
9
+ import { listSnapshots as listSnapshotsEffect } from "./listSnapshotsEffect.js";
10
+ export { parseSnapshot } from "./parseSnapshot.js";
11
+ export { captureSnapshotEffect, listSnapshotsEffect, loadLatestSnapshotEffect, loadSnapshotEffect, };
12
+
13
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
14
+
15
+ /**
16
+ * Capture a snapshot row for a run at a given frame.
17
+ *
18
+ * @param {SmithersDb} adapter
19
+ * @param {string} runId
20
+ * @param {number} frameNo
21
+ * @param {SnapshotData} data
22
+ * @returns {Promise<Snapshot>}
23
+ */
24
+ export function captureSnapshot(adapter, runId, frameNo, data) {
25
+ return Effect.runPromise(captureSnapshotEffect(adapter, runId, frameNo, data));
26
+ }
27
+ /**
28
+ * Load a specific snapshot row for a run/frame.
29
+ *
30
+ * @param {SmithersDb} adapter
31
+ * @param {string} runId
32
+ * @param {number} frameNo
33
+ * @returns {Promise<Snapshot | undefined>}
34
+ */
35
+ export function loadSnapshot(adapter, runId, frameNo) {
36
+ return Effect.runPromise(loadSnapshotEffect(adapter, runId, frameNo));
37
+ }
38
+ /**
39
+ * Load the most recent snapshot row for a run.
40
+ *
41
+ * @param {SmithersDb} adapter
42
+ * @param {string} runId
43
+ * @returns {Promise<Snapshot | undefined>}
44
+ */
45
+ export function loadLatestSnapshot(adapter, runId) {
46
+ return Effect.runPromise(loadLatestSnapshotEffect(adapter, runId));
47
+ }
48
+ /**
49
+ * List lightweight snapshot index rows for a run.
50
+ *
51
+ * @param {SmithersDb} adapter
52
+ * @param {string} runId
53
+ * @returns {Promise<Array<Pick<Snapshot, "runId" | "frameNo" | "contentHash" | "createdAtMs" | "vcsPointer">>>}
54
+ */
55
+ export function listSnapshots(adapter, runId) {
56
+ return Effect.runPromise(listSnapshotsEffect(adapter, runId));
57
+ }
@@ -0,0 +1,32 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { Effect } from "effect";
3
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
4
+ import { smithersSnapshots } from "../schema.js";
5
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
6
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
7
+ /** @typedef {import("./Snapshot.ts").Snapshot} Snapshot */
8
+
9
+ /**
10
+ * @param {SmithersDb} adapter
11
+ * @param {string} runId
12
+ * @returns {Effect.Effect<Array<Pick<Snapshot, "runId" | "frameNo" | "contentHash" | "createdAtMs" | "vcsPointer">>, SmithersError>}
13
+ */
14
+ export function listSnapshots(adapter, runId) {
15
+ return Effect.tryPromise({
16
+ try: () => adapter.db
17
+ .select({
18
+ runId: smithersSnapshots.runId,
19
+ frameNo: smithersSnapshots.frameNo,
20
+ contentHash: smithersSnapshots.contentHash,
21
+ createdAtMs: smithersSnapshots.createdAtMs,
22
+ vcsPointer: smithersSnapshots.vcsPointer,
23
+ })
24
+ .from(smithersSnapshots)
25
+ .where(eq(smithersSnapshots.runId, runId))
26
+ .orderBy(smithersSnapshots.frameNo),
27
+ catch: (cause) => toSmithersError(cause, "list snapshots", {
28
+ code: "DB_QUERY_FAILED",
29
+ details: { runId },
30
+ }),
31
+ }).pipe(Effect.annotateLogs({ runId }), Effect.withLogSpan("time-travel:list-snapshots"));
32
+ }
@@ -0,0 +1,46 @@
1
+ import { and, desc, eq } from "drizzle-orm";
2
+ import { Effect } from "effect";
3
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
4
+ import { smithersSnapshots } from "../schema.js";
5
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
6
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
7
+ /** @typedef {import("./Snapshot.ts").Snapshot} Snapshot */
8
+
9
+ /**
10
+ * @param {SmithersDb} adapter
11
+ * @param {string} runId
12
+ * @param {number} frameNo
13
+ * @returns {Effect.Effect<Snapshot | undefined, SmithersError>}
14
+ */
15
+ export function loadSnapshot(adapter, runId, frameNo) {
16
+ return Effect.tryPromise({
17
+ try: () => adapter.db
18
+ .select()
19
+ .from(smithersSnapshots)
20
+ .where(and(eq(smithersSnapshots.runId, runId), eq(smithersSnapshots.frameNo, frameNo)))
21
+ .limit(1),
22
+ catch: (cause) => toSmithersError(cause, "load snapshot", {
23
+ code: "DB_QUERY_FAILED",
24
+ details: { frameNo, runId },
25
+ }),
26
+ }).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({ runId, frameNo: String(frameNo) }), Effect.withLogSpan("time-travel:load-snapshot"));
27
+ }
28
+ /**
29
+ * @param {SmithersDb} adapter
30
+ * @param {string} runId
31
+ * @returns {Effect.Effect<Snapshot | undefined, SmithersError>}
32
+ */
33
+ export function loadLatestSnapshot(adapter, runId) {
34
+ return Effect.tryPromise({
35
+ try: () => adapter.db
36
+ .select()
37
+ .from(smithersSnapshots)
38
+ .where(eq(smithersSnapshots.runId, runId))
39
+ .orderBy(desc(smithersSnapshots.frameNo))
40
+ .limit(1),
41
+ catch: (cause) => toSmithersError(cause, "load latest snapshot", {
42
+ code: "DB_QUERY_FAILED",
43
+ details: { runId },
44
+ }),
45
+ }).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({ runId }), Effect.withLogSpan("time-travel:load-latest-snapshot"));
46
+ }
@@ -0,0 +1,31 @@
1
+
2
+ /** @typedef {import("../ParsedSnapshot.ts").ParsedSnapshot} ParsedSnapshot */
3
+ /** @typedef {import("./Snapshot.ts").Snapshot} Snapshot */
4
+ /**
5
+ * @param {Snapshot} snapshot
6
+ * @returns {ParsedSnapshot}
7
+ */
8
+ export function parseSnapshot(snapshot) {
9
+ const nodesArr = JSON.parse(snapshot.nodesJson);
10
+ const nodes = {};
11
+ for (const n of nodesArr) {
12
+ nodes[`${n.nodeId}::${n.iteration}`] = n;
13
+ }
14
+ const ralphArr = JSON.parse(snapshot.ralphJson);
15
+ const ralph = {};
16
+ for (const r of ralphArr) {
17
+ ralph[r.ralphId] = r;
18
+ }
19
+ return {
20
+ runId: snapshot.runId,
21
+ frameNo: snapshot.frameNo,
22
+ nodes,
23
+ outputs: JSON.parse(snapshot.outputsJson),
24
+ ralph,
25
+ input: JSON.parse(snapshot.inputJson),
26
+ vcsPointer: snapshot.vcsPointer,
27
+ workflowHash: snapshot.workflowHash,
28
+ contentHash: snapshot.contentHash,
29
+ createdAtMs: snapshot.createdAtMs,
30
+ };
31
+ }
@@ -0,0 +1,7 @@
1
+ import { Metric, MetricBoundaries } from "effect";
2
+ const snapshotBuckets = MetricBoundaries.exponential({
3
+ start: 1,
4
+ factor: 2,
5
+ count: 12,
6
+ }); // ~1ms to ~2s
7
+ export const snapshotDuration = Metric.histogram("smithers.snapshot.duration_ms", snapshotBuckets);
@@ -0,0 +1,2 @@
1
+ import { Metric } from "effect";
2
+ export const snapshotsCaptured = Metric.counter("smithers.snapshots.captured");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @param {number} ms
3
+ * @returns {string}
4
+ */
5
+ export function formatTimestamp(ms) {
6
+ return new Date(ms).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
7
+ }
@@ -0,0 +1,38 @@
1
+ import { Effect } from "effect";
2
+ import { listSnapshots } from "../snapshot/listSnapshotsEffect.js";
3
+ import { listBranches } from "../fork/listBranchesEffect.js";
4
+ import { getBranchInfo } from "../fork/getBranchInfoEffect.js";
5
+ /** @typedef {import("../RunTimeline.ts").RunTimeline} RunTimeline */
6
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
7
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
8
+
9
+ /**
10
+ * @param {SmithersDb} adapter
11
+ * @param {string} runId
12
+ * @returns {Effect.Effect<RunTimeline, SmithersError>}
13
+ */
14
+ export function buildTimeline(adapter, runId) {
15
+ return Effect.gen(function* () {
16
+ const snapshots = yield* listSnapshots(adapter, runId);
17
+ const branches = yield* listBranches(adapter, runId);
18
+ const ownBranch = yield* getBranchInfo(adapter, runId);
19
+ // Index branches by parent frame number for fast lookup
20
+ const branchByFrame = new Map();
21
+ for (const b of branches) {
22
+ const existing = branchByFrame.get(b.parentFrameNo) ?? [];
23
+ existing.push(b);
24
+ branchByFrame.set(b.parentFrameNo, existing);
25
+ }
26
+ const frames = snapshots.map((s) => ({
27
+ frameNo: s.frameNo,
28
+ createdAtMs: s.createdAtMs,
29
+ contentHash: s.contentHash,
30
+ forkPoints: branchByFrame.get(s.frameNo) ?? [],
31
+ }));
32
+ return {
33
+ runId,
34
+ frames,
35
+ branch: ownBranch ?? null,
36
+ };
37
+ }).pipe(Effect.annotateLogs({ runId }), Effect.withLogSpan("time-travel:build-timeline"));
38
+ }
@@ -0,0 +1,30 @@
1
+ import { Effect } from "effect";
2
+ import { buildTimeline } from "./buildTimelineEffect.js";
3
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
4
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
5
+ /** @typedef {import("../TimelineTree.ts").TimelineTree} TimelineTree */
6
+
7
+ /**
8
+ * @param {SmithersDb} adapter
9
+ * @param {string} runId
10
+ * @returns {Effect.Effect<TimelineTree, SmithersError>}
11
+ */
12
+ export function buildTimelineTree(adapter, runId) {
13
+ return Effect.gen(function* () {
14
+ const timeline = yield* buildTimeline(adapter, runId);
15
+ // Collect all child runs that branch from this run
16
+ const childRunIds = [];
17
+ for (const frame of timeline.frames) {
18
+ for (const fork of frame.forkPoints) {
19
+ childRunIds.push(fork.runId);
20
+ }
21
+ }
22
+ // Recursively build subtrees
23
+ const children = [];
24
+ for (const childId of childRunIds) {
25
+ const childTree = yield* buildTimelineTree(adapter, childId);
26
+ children.push(childTree);
27
+ }
28
+ return { timeline, children };
29
+ }).pipe(Effect.annotateLogs({ runId }), Effect.withLogSpan("time-travel:build-timeline-tree"));
30
+ }
@@ -0,0 +1,23 @@
1
+
2
+ /** @typedef {import("../TimelineTree.ts").TimelineTree} TimelineTree */
3
+ /**
4
+ * @param {TimelineTree} tree
5
+ * @returns {object}
6
+ */
7
+ export function formatTimelineAsJson(tree) {
8
+ return {
9
+ runId: tree.timeline.runId,
10
+ branch: tree.timeline.branch,
11
+ frames: tree.timeline.frames.map((f) => ({
12
+ frameNo: f.frameNo,
13
+ createdAtMs: f.createdAtMs,
14
+ contentHash: f.contentHash,
15
+ forks: f.forkPoints.map((fp) => ({
16
+ runId: fp.runId,
17
+ branchLabel: fp.branchLabel,
18
+ forkDescription: fp.forkDescription,
19
+ })),
20
+ })),
21
+ children: tree.children.map(formatTimelineAsJson),
22
+ };
23
+ }
@@ -0,0 +1,31 @@
1
+ import pc from "picocolors";
2
+ import { formatTimestamp } from "./_helpers.js";
3
+ /** @typedef {import("../TimelineTree.ts").TimelineTree} TimelineTree */
4
+
5
+ /**
6
+ * @param {TimelineTree} tree
7
+ * @returns {string}
8
+ */
9
+ export function formatTimelineForTui(tree, indent = 0) {
10
+ const lines = [];
11
+ const pad = " ".repeat(indent);
12
+ const tl = tree.timeline;
13
+ const labelSuffix = tl.branch
14
+ ? ` ${pc.dim(`[${tl.branch.branchLabel ?? "fork"}]`)} ${pc.dim(`(forked from ${tl.branch.parentRunId.slice(0, 8)}:${tl.branch.parentFrameNo})`)}`
15
+ : "";
16
+ lines.push(`${pad}${pc.bold(tl.runId)}${labelSuffix}`);
17
+ for (const frame of tl.frames) {
18
+ const ts = formatTimestamp(frame.createdAtMs);
19
+ const hash = frame.contentHash.slice(0, 8);
20
+ lines.push(`${pad} Frame ${frame.frameNo} ${pc.dim(ts)} ${pc.dim(hash)}`);
21
+ // Show fork points after the frame
22
+ for (const fork of frame.forkPoints) {
23
+ const childTree = tree.children.find((c) => c.timeline.runId === fork.runId);
24
+ if (childTree) {
25
+ lines.push(`${pad} ${pc.yellow("|--")} ${pc.cyan(fork.runId.slice(0, 12))} ${pc.dim(`[${fork.branchLabel ?? "fork"}]`)} ${pc.dim(`(forked at frame ${fork.parentFrameNo})`)}`);
26
+ lines.push(formatTimelineForTui(childTree, indent + 2));
27
+ }
28
+ }
29
+ }
30
+ return lines.join("\n");
31
+ }
@@ -0,0 +1,31 @@
1
+ import { Effect } from "effect";
2
+ import { buildTimeline as buildTimelineEffect } from "./buildTimelineEffect.js";
3
+ import { buildTimelineTree as buildTimelineTreeEffect } from "./buildTimelineTreeEffect.js";
4
+ export { formatTimelineForTui } from "./formatTimelineForTui.js";
5
+ export { formatTimelineAsJson } from "./formatTimelineAsJson.js";
6
+ export { buildTimelineEffect, buildTimelineTreeEffect, };
7
+
8
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
9
+ /** @typedef {import("../RunTimeline.ts").RunTimeline} RunTimeline */
10
+ /** @typedef {import("../TimelineTree.ts").TimelineTree} TimelineTree */
11
+
12
+ /**
13
+ * Build the flat timeline (snapshots + branches) for a run.
14
+ *
15
+ * @param {SmithersDb} adapter
16
+ * @param {string} runId
17
+ * @returns {Promise<RunTimeline>}
18
+ */
19
+ export function buildTimeline(adapter, runId) {
20
+ return Effect.runPromise(buildTimelineEffect(adapter, runId));
21
+ }
22
+ /**
23
+ * Build the recursive timeline tree (run + all descendants) for a run.
24
+ *
25
+ * @param {SmithersDb} adapter
26
+ * @param {string} runId
27
+ * @returns {Promise<TimelineTree>}
28
+ */
29
+ export function buildTimelineTree(adapter, runId) {
30
+ return Effect.runPromise(buildTimelineTreeEffect(adapter, runId));
31
+ }