@nebutra/time-machine 0.1.1
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/.nebutra/debug/content-store.jsonl +4 -0
- package/.nebutra/debug/event-log.jsonl +7 -0
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-test.log +16 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +676 -0
- package/README.md +28 -0
- package/dist/chunk-AQ6XF75L.js +172 -0
- package/dist/chunk-AQ6XF75L.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +36 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/examples/branch.ts +22 -0
- package/examples/doctor.ts +9 -0
- package/examples/quickstart.ts +10 -0
- package/package.json +62 -0
- package/plays/timeline_view/SKILL.md +23 -0
- package/src/cli.ts +28 -0
- package/src/index.test.ts +85 -0
- package/src/index.ts +255 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @nebutra/time-machine
|
|
2
|
+
|
|
3
|
+
Status: **WIP**
|
|
4
|
+
|
|
5
|
+
Chronos product surface for founder-facing timeline views, branches, comparisons,
|
|
6
|
+
annotations, and rollback dry-runs.
|
|
7
|
+
|
|
8
|
+
## Boundary
|
|
9
|
+
|
|
10
|
+
`time-machine` does not own historical truth. Event truth stays in
|
|
11
|
+
`@nebutra/event-log`; this package projects it into product data and writes only
|
|
12
|
+
annotation artifacts to `@nebutra/content-store`.
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm time-machine:doctor
|
|
18
|
+
pnpm time-machine:quickstart
|
|
19
|
+
pnpm time-machine:debug
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Current Closure
|
|
23
|
+
|
|
24
|
+
- Timeline projection over event-log.
|
|
25
|
+
- Branch creation through event-log.
|
|
26
|
+
- Compare narrative over affected assets.
|
|
27
|
+
- Rollback dry-run only.
|
|
28
|
+
- Star annotations as content-store artifacts.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
5
|
+
import { ContentStore } from "@nebutra/content-store";
|
|
6
|
+
import { CapabilityError } from "@nebutra/errors";
|
|
7
|
+
import { EventLog } from "@nebutra/event-log";
|
|
8
|
+
import { assetId } from "@nebutra/generation-context";
|
|
9
|
+
function requireTenant(value) {
|
|
10
|
+
if (!value?.trim()) {
|
|
11
|
+
throw new CapabilityError("time-machine", "Time Machine requires tenant context", {
|
|
12
|
+
suggestion: "Pass tenantId when opening TimeMachine so timeline reads stay tenant-scoped.",
|
|
13
|
+
statusCode: 400
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function nodeKind(event) {
|
|
19
|
+
if (event.traceId.includes("branch")) return "branch";
|
|
20
|
+
if (event.traceId.includes("decision")) return "decision";
|
|
21
|
+
if (event.traceId.includes("play")) return "play_completed";
|
|
22
|
+
return "milestone";
|
|
23
|
+
}
|
|
24
|
+
function toNode(event) {
|
|
25
|
+
return {
|
|
26
|
+
id: `node_${event.id}`,
|
|
27
|
+
eventId: event.id,
|
|
28
|
+
kind: nodeKind(event),
|
|
29
|
+
timestamp: event.at,
|
|
30
|
+
oneLineSummary: event.summary,
|
|
31
|
+
branchId: "main",
|
|
32
|
+
...event.parent ? { parentNode: `node_${event.parent}` } : {},
|
|
33
|
+
affectedAssets: event.affected,
|
|
34
|
+
preview: { kind: "event-summary", text: event.summary }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function unique(values) {
|
|
38
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
39
|
+
}
|
|
40
|
+
var TimeMachine = class _TimeMachine {
|
|
41
|
+
#tenantId;
|
|
42
|
+
#debugRoot;
|
|
43
|
+
#contentStore;
|
|
44
|
+
#eventLog;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.#tenantId = requireTenant(options.tenantId);
|
|
47
|
+
this.#debugRoot = options.debugRoot ?? process.cwd();
|
|
48
|
+
this.#contentStore = options.contentStore;
|
|
49
|
+
this.#eventLog = options.eventLog;
|
|
50
|
+
}
|
|
51
|
+
static async open(root = ".nebutra/time-machine", options = {}) {
|
|
52
|
+
const tenantId = options.tenantId ?? "local";
|
|
53
|
+
await mkdir(root, { recursive: true });
|
|
54
|
+
const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
|
|
55
|
+
const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
|
|
56
|
+
return new _TimeMachine({ ...options, tenantId, contentStore, eventLog });
|
|
57
|
+
}
|
|
58
|
+
static async fromStores(options) {
|
|
59
|
+
if (options.root) await mkdir(options.root, { recursive: true });
|
|
60
|
+
return new _TimeMachine({
|
|
61
|
+
tenantId: options.tenantId,
|
|
62
|
+
contentStore: options.contentStore,
|
|
63
|
+
eventLog: options.eventLog,
|
|
64
|
+
...(options.debugRoot ?? options.root) !== void 0 ? { debugRoot: options.debugRoot ?? options.root } : {}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async timelineView() {
|
|
68
|
+
const events = await this.#eventLog.timeline();
|
|
69
|
+
const nodes = events.map(toNode);
|
|
70
|
+
await this.#debug({ type: "timeline_view", tenantId: this.#tenantId, nodes: nodes.length });
|
|
71
|
+
return { tenantId: this.#tenantId, truthSource: "event-log", nodes };
|
|
72
|
+
}
|
|
73
|
+
async branchFrom(eventId, name) {
|
|
74
|
+
const branch = await this.#eventLog.branchFrom(eventId, name);
|
|
75
|
+
const result = {
|
|
76
|
+
branchId: assetId("branch", `${eventId}_${name}`),
|
|
77
|
+
name: branch.name,
|
|
78
|
+
fromEventId: branch.from,
|
|
79
|
+
createdAt: branch.at
|
|
80
|
+
};
|
|
81
|
+
await this.#debug({ type: "branch", tenantId: this.#tenantId, eventId, name });
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
async star(eventId, label = "starred") {
|
|
85
|
+
await this.#requireEvent(eventId);
|
|
86
|
+
const annotation = {
|
|
87
|
+
eventId,
|
|
88
|
+
starred: true,
|
|
89
|
+
label,
|
|
90
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
91
|
+
truthSource: "event-log"
|
|
92
|
+
};
|
|
93
|
+
const path = `time-machine/annotations/${eventId}.json`;
|
|
94
|
+
await this.#contentStore.write(path, `${JSON.stringify(annotation, null, 2)}
|
|
95
|
+
`);
|
|
96
|
+
await this.#eventLog.commit({
|
|
97
|
+
traceId: assetId("time_machine_annotation", eventId),
|
|
98
|
+
kind: "content_write",
|
|
99
|
+
affected: [path],
|
|
100
|
+
parent: eventId,
|
|
101
|
+
snapshot: { [path]: `${JSON.stringify(annotation, null, 2)}
|
|
102
|
+
` }
|
|
103
|
+
});
|
|
104
|
+
await this.#debug({ type: "star", tenantId: this.#tenantId, eventId, label });
|
|
105
|
+
return { eventId, starred: true, label, path };
|
|
106
|
+
}
|
|
107
|
+
async compare(leftEventId, rightEventId) {
|
|
108
|
+
const left = await this.#requireEvent(leftEventId);
|
|
109
|
+
const right = await this.#requireEvent(rightEventId);
|
|
110
|
+
const changedAssets = unique([...left.affected, ...right.affected]);
|
|
111
|
+
const narrative = `Compared ${leftEventId} to ${rightEventId}: ${changedAssets.join(
|
|
112
|
+
", "
|
|
113
|
+
)} changed.`;
|
|
114
|
+
await this.#debug({
|
|
115
|
+
type: "compare",
|
|
116
|
+
tenantId: this.#tenantId,
|
|
117
|
+
leftEventId,
|
|
118
|
+
rightEventId,
|
|
119
|
+
changedAssets
|
|
120
|
+
});
|
|
121
|
+
return { leftEventId, rightEventId, changedAssets, narrative };
|
|
122
|
+
}
|
|
123
|
+
async rollbackDryRun(eventId) {
|
|
124
|
+
const plan = await this.#eventLog.rollbackTo(eventId);
|
|
125
|
+
await this.#debug({
|
|
126
|
+
type: "rollback_dry_run",
|
|
127
|
+
tenantId: this.#tenantId,
|
|
128
|
+
eventId,
|
|
129
|
+
affected: plan.affected
|
|
130
|
+
});
|
|
131
|
+
return plan;
|
|
132
|
+
}
|
|
133
|
+
async doctor() {
|
|
134
|
+
const events = await this.#eventLog.timeline();
|
|
135
|
+
const report = {
|
|
136
|
+
capability: "time-machine",
|
|
137
|
+
ok: true,
|
|
138
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
139
|
+
truthSource: "event-log",
|
|
140
|
+
features: ["timeline", "branch", "compare", "rollback-dry-run", "annotation"],
|
|
141
|
+
...events.length === 0 ? { suggestion: "Commit at least one event to Chronos before opening the star-map view." } : {}
|
|
142
|
+
};
|
|
143
|
+
await this.#debug({ type: "doctor", tenantId: this.#tenantId, events: events.length });
|
|
144
|
+
return report;
|
|
145
|
+
}
|
|
146
|
+
async close() {
|
|
147
|
+
await this.#contentStore.close();
|
|
148
|
+
}
|
|
149
|
+
async #requireEvent(eventId) {
|
|
150
|
+
const event = (await this.#eventLog.timeline()).find((record) => record.id === eventId);
|
|
151
|
+
if (!event) {
|
|
152
|
+
throw new CapabilityError("time-machine", "Timeline event not found", {
|
|
153
|
+
suggestion: "Call timelineView and choose an event id from the current tenant.",
|
|
154
|
+
metadata: { eventId },
|
|
155
|
+
statusCode: 404
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return event;
|
|
159
|
+
}
|
|
160
|
+
async #debug(entry) {
|
|
161
|
+
await appendCapabilityDebug("time-machine", entry, { root: this.#debugRoot });
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
async function readTimeMachineDebug(root = process.cwd(), limit = 20) {
|
|
165
|
+
return readCapabilityDebug("time-machine", { root, limit });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export {
|
|
169
|
+
TimeMachine,
|
|
170
|
+
readTimeMachineDebug
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=chunk-AQ6XF75L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { appendCapabilityDebug, readCapabilityDebug } from \"@nebutra/capability-kit/debug\";\nimport { ContentStore } from \"@nebutra/content-store\";\nimport { CapabilityError } from \"@nebutra/errors\";\nimport { EventLog, type EventRecord, type RollbackPlan } from \"@nebutra/event-log\";\nimport { assetId } from \"@nebutra/generation-context\";\n\nexport type TimelineNodeKind = \"milestone\" | \"decision\" | \"branch\" | \"play_completed\";\n\nexport interface TimelineNode {\n readonly id: string;\n readonly eventId: string;\n readonly kind: TimelineNodeKind;\n readonly timestamp: string;\n readonly oneLineSummary: string;\n readonly branchId: string;\n readonly parentNode?: string;\n readonly affectedAssets: readonly string[];\n readonly preview: {\n readonly kind: \"event-summary\";\n readonly text: string;\n };\n}\n\nexport interface TimelineView {\n readonly tenantId: string;\n readonly truthSource: \"event-log\";\n readonly nodes: readonly TimelineNode[];\n}\n\nexport interface TimelineBranch {\n readonly branchId: string;\n readonly name: string;\n readonly fromEventId: string;\n readonly createdAt: string;\n}\n\nexport interface TimelineAnnotation {\n readonly eventId: string;\n readonly starred: true;\n readonly label: string;\n readonly path: string;\n}\n\nexport interface TimelineComparison {\n readonly leftEventId: string;\n readonly rightEventId: string;\n readonly changedAssets: readonly string[];\n readonly narrative: string;\n}\n\nexport interface TimeMachineDoctorReport {\n readonly capability: \"time-machine\";\n readonly ok: boolean;\n readonly checkedAt: string;\n readonly truthSource: \"event-log\";\n readonly features: readonly string[];\n readonly suggestion?: string;\n}\n\nexport interface TimeMachineOptions {\n readonly tenantId?: string;\n readonly root?: string;\n readonly debugRoot?: string;\n readonly contentStore?: ContentStore;\n readonly eventLog?: EventLog;\n}\n\nfunction requireTenant(value: string | undefined): string {\n if (!value?.trim()) {\n throw new CapabilityError(\"time-machine\", \"Time Machine requires tenant context\", {\n suggestion: \"Pass tenantId when opening TimeMachine so timeline reads stay tenant-scoped.\",\n statusCode: 400,\n });\n }\n return value;\n}\n\nfunction nodeKind(event: EventRecord): TimelineNodeKind {\n if (event.traceId.includes(\"branch\")) return \"branch\";\n if (event.traceId.includes(\"decision\")) return \"decision\";\n if (event.traceId.includes(\"play\")) return \"play_completed\";\n return \"milestone\";\n}\n\nfunction toNode(event: EventRecord): TimelineNode {\n return {\n id: `node_${event.id}`,\n eventId: event.id,\n kind: nodeKind(event),\n timestamp: event.at,\n oneLineSummary: event.summary,\n branchId: \"main\",\n ...(event.parent ? { parentNode: `node_${event.parent}` } : {}),\n affectedAssets: event.affected,\n preview: { kind: \"event-summary\", text: event.summary },\n };\n}\n\nfunction unique(values: readonly string[]): string[] {\n return [...new Set(values)].sort((left, right) => left.localeCompare(right));\n}\n\nexport class TimeMachine {\n readonly #tenantId: string;\n readonly #debugRoot: string;\n readonly #contentStore: ContentStore;\n readonly #eventLog: EventLog;\n\n private constructor(\n options: Required<Pick<TimeMachineOptions, \"tenantId\" | \"contentStore\" | \"eventLog\">> &\n Pick<TimeMachineOptions, \"debugRoot\">,\n ) {\n this.#tenantId = requireTenant(options.tenantId);\n this.#debugRoot = options.debugRoot ?? process.cwd();\n this.#contentStore = options.contentStore;\n this.#eventLog = options.eventLog;\n }\n\n static async open(\n root = \".nebutra/time-machine\",\n options: Omit<TimeMachineOptions, \"root\" | \"contentStore\" | \"eventLog\"> = {},\n ): Promise<TimeMachine> {\n const tenantId = options.tenantId ?? \"local\";\n await mkdir(root, { recursive: true });\n const contentStore = await ContentStore.open(join(root, \"content\"), { tenantId });\n const eventLog = await EventLog.open(join(root, \"event-log\"), { tenantId });\n return new TimeMachine({ ...options, tenantId, contentStore, eventLog });\n }\n\n static async fromStores(\n options: Required<Pick<TimeMachineOptions, \"tenantId\" | \"contentStore\" | \"eventLog\">> &\n Pick<TimeMachineOptions, \"root\" | \"debugRoot\">,\n ): Promise<TimeMachine> {\n if (options.root) await mkdir(options.root, { recursive: true });\n return new TimeMachine({\n tenantId: options.tenantId,\n contentStore: options.contentStore,\n eventLog: options.eventLog,\n ...((options.debugRoot ?? options.root) !== undefined\n ? { debugRoot: options.debugRoot ?? options.root }\n : {}),\n });\n }\n\n async timelineView(): Promise<TimelineView> {\n const events = await this.#eventLog.timeline();\n const nodes = events.map(toNode);\n await this.#debug({ type: \"timeline_view\", tenantId: this.#tenantId, nodes: nodes.length });\n return { tenantId: this.#tenantId, truthSource: \"event-log\", nodes };\n }\n\n async branchFrom(eventId: string, name: string): Promise<TimelineBranch> {\n const branch = await this.#eventLog.branchFrom(eventId, name);\n const result = {\n branchId: assetId(\"branch\", `${eventId}_${name}`),\n name: branch.name,\n fromEventId: branch.from,\n createdAt: branch.at,\n };\n await this.#debug({ type: \"branch\", tenantId: this.#tenantId, eventId, name });\n return result;\n }\n\n async star(eventId: string, label = \"starred\"): Promise<TimelineAnnotation> {\n await this.#requireEvent(eventId);\n const annotation = {\n eventId,\n starred: true,\n label,\n createdAt: new Date().toISOString(),\n truthSource: \"event-log\",\n };\n const path = `time-machine/annotations/${eventId}.json`;\n await this.#contentStore.write(path, `${JSON.stringify(annotation, null, 2)}\\n`);\n await this.#eventLog.commit({\n traceId: assetId(\"time_machine_annotation\", eventId),\n kind: \"content_write\",\n affected: [path],\n parent: eventId,\n snapshot: { [path]: `${JSON.stringify(annotation, null, 2)}\\n` },\n });\n await this.#debug({ type: \"star\", tenantId: this.#tenantId, eventId, label });\n return { eventId, starred: true, label, path };\n }\n\n async compare(leftEventId: string, rightEventId: string): Promise<TimelineComparison> {\n const left = await this.#requireEvent(leftEventId);\n const right = await this.#requireEvent(rightEventId);\n const changedAssets = unique([...left.affected, ...right.affected]);\n const narrative = `Compared ${leftEventId} to ${rightEventId}: ${changedAssets.join(\n \", \",\n )} changed.`;\n await this.#debug({\n type: \"compare\",\n tenantId: this.#tenantId,\n leftEventId,\n rightEventId,\n changedAssets,\n });\n return { leftEventId, rightEventId, changedAssets, narrative };\n }\n\n async rollbackDryRun(eventId: string): Promise<RollbackPlan> {\n const plan = await this.#eventLog.rollbackTo(eventId);\n await this.#debug({\n type: \"rollback_dry_run\",\n tenantId: this.#tenantId,\n eventId,\n affected: plan.affected,\n });\n return plan;\n }\n\n async doctor(): Promise<TimeMachineDoctorReport> {\n const events = await this.#eventLog.timeline();\n const report: TimeMachineDoctorReport = {\n capability: \"time-machine\",\n ok: true,\n checkedAt: new Date().toISOString(),\n truthSource: \"event-log\",\n features: [\"timeline\", \"branch\", \"compare\", \"rollback-dry-run\", \"annotation\"],\n ...(events.length === 0\n ? { suggestion: \"Commit at least one event to Chronos before opening the star-map view.\" }\n : {}),\n };\n await this.#debug({ type: \"doctor\", tenantId: this.#tenantId, events: events.length });\n return report;\n }\n\n async close(): Promise<void> {\n await this.#contentStore.close();\n }\n\n async #requireEvent(eventId: string): Promise<EventRecord> {\n const event = (await this.#eventLog.timeline()).find((record) => record.id === eventId);\n if (!event) {\n throw new CapabilityError(\"time-machine\", \"Timeline event not found\", {\n suggestion: \"Call timelineView and choose an event id from the current tenant.\",\n metadata: { eventId },\n statusCode: 404,\n });\n }\n return event;\n }\n\n async #debug(entry: Record<string, unknown>): Promise<void> {\n await appendCapabilityDebug(\"time-machine\", entry, { root: this.#debugRoot });\n }\n}\n\nexport async function readTimeMachineDebug(root = process.cwd(), limit = 20): Promise<unknown[]> {\n return readCapabilityDebug(\"time-machine\", { root, limit });\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TimeMachine,
|
|
3
|
+
readTimeMachineDebug
|
|
4
|
+
} from "./chunk-AQ6XF75L.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
var command = process.argv[2] ?? "doctor";
|
|
8
|
+
var root = process.env.TIME_MACHINE_ROOT ?? ".nebutra/time-machine";
|
|
9
|
+
var tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
|
|
10
|
+
if (command === "doctor") {
|
|
11
|
+
const machine = await TimeMachine.open(root, { tenantId });
|
|
12
|
+
try {
|
|
13
|
+
process.stdout.write(`${JSON.stringify(await machine.doctor(), null, 2)}
|
|
14
|
+
`);
|
|
15
|
+
} finally {
|
|
16
|
+
await machine.close();
|
|
17
|
+
}
|
|
18
|
+
} else if (command === "quickstart") {
|
|
19
|
+
const machine = await TimeMachine.open(root, { tenantId });
|
|
20
|
+
try {
|
|
21
|
+
process.stdout.write(`${JSON.stringify(await machine.timelineView(), null, 2)}
|
|
22
|
+
`);
|
|
23
|
+
} finally {
|
|
24
|
+
await machine.close();
|
|
25
|
+
}
|
|
26
|
+
} else if (command === "debug") {
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
`${JSON.stringify({ capability: "time-machine", entries: await readTimeMachineDebug() }, null, 2)}
|
|
29
|
+
`
|
|
30
|
+
);
|
|
31
|
+
} else {
|
|
32
|
+
process.stderr.write(`Unknown time-machine command: ${command}
|
|
33
|
+
`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { readTimeMachineDebug, TimeMachine } from \"./index\";\n\nconst command = process.argv[2] ?? \"doctor\";\nconst root = process.env.TIME_MACHINE_ROOT ?? \".nebutra/time-machine\";\nconst tenantId = process.env.NEBUTRA_TENANT_ID ?? \"local\";\n\nif (command === \"doctor\") {\n const machine = await TimeMachine.open(root, { tenantId });\n try {\n process.stdout.write(`${JSON.stringify(await machine.doctor(), null, 2)}\\n`);\n } finally {\n await machine.close();\n }\n} else if (command === \"quickstart\") {\n const machine = await TimeMachine.open(root, { tenantId });\n try {\n process.stdout.write(`${JSON.stringify(await machine.timelineView(), null, 2)}\\n`);\n } finally {\n await machine.close();\n }\n} else if (command === \"debug\") {\n process.stdout.write(\n `${JSON.stringify({ capability: \"time-machine\", entries: await readTimeMachineDebug() }, null, 2)}\\n`,\n );\n} else {\n process.stderr.write(`Unknown time-machine command: ${command}\\n`);\n process.exitCode = 1;\n}\n"],"mappings":";;;;;;AAEA,IAAM,UAAU,QAAQ,KAAK,CAAC,KAAK;AACnC,IAAM,OAAO,QAAQ,IAAI,qBAAqB;AAC9C,IAAM,WAAW,QAAQ,IAAI,qBAAqB;AAElD,IAAI,YAAY,UAAU;AACxB,QAAM,UAAU,MAAM,YAAY,KAAK,MAAM,EAAE,SAAS,CAAC;AACzD,MAAI;AACF,YAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,MAAM,QAAQ,OAAO,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EAC7E,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF,WAAW,YAAY,cAAc;AACnC,QAAM,UAAU,MAAM,YAAY,KAAK,MAAM,EAAE,SAAS,CAAC;AACzD,MAAI;AACF,YAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,MAAM,QAAQ,aAAa,GAAG,MAAM,CAAC,CAAC;AAAA,CAAI;AAAA,EACnF,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF,WAAW,YAAY,SAAS;AAC9B,UAAQ,OAAO;AAAA,IACb,GAAG,KAAK,UAAU,EAAE,YAAY,gBAAgB,SAAS,MAAM,qBAAqB,EAAE,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,EACnG;AACF,OAAO;AACL,UAAQ,OAAO,MAAM,iCAAiC,OAAO;AAAA,CAAI;AACjE,UAAQ,WAAW;AACrB;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ContentStore } from '@nebutra/content-store';
|
|
2
|
+
import { EventLog, RollbackPlan } from '@nebutra/event-log';
|
|
3
|
+
|
|
4
|
+
type TimelineNodeKind = "milestone" | "decision" | "branch" | "play_completed";
|
|
5
|
+
interface TimelineNode {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly eventId: string;
|
|
8
|
+
readonly kind: TimelineNodeKind;
|
|
9
|
+
readonly timestamp: string;
|
|
10
|
+
readonly oneLineSummary: string;
|
|
11
|
+
readonly branchId: string;
|
|
12
|
+
readonly parentNode?: string;
|
|
13
|
+
readonly affectedAssets: readonly string[];
|
|
14
|
+
readonly preview: {
|
|
15
|
+
readonly kind: "event-summary";
|
|
16
|
+
readonly text: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
interface TimelineView {
|
|
20
|
+
readonly tenantId: string;
|
|
21
|
+
readonly truthSource: "event-log";
|
|
22
|
+
readonly nodes: readonly TimelineNode[];
|
|
23
|
+
}
|
|
24
|
+
interface TimelineBranch {
|
|
25
|
+
readonly branchId: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly fromEventId: string;
|
|
28
|
+
readonly createdAt: string;
|
|
29
|
+
}
|
|
30
|
+
interface TimelineAnnotation {
|
|
31
|
+
readonly eventId: string;
|
|
32
|
+
readonly starred: true;
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly path: string;
|
|
35
|
+
}
|
|
36
|
+
interface TimelineComparison {
|
|
37
|
+
readonly leftEventId: string;
|
|
38
|
+
readonly rightEventId: string;
|
|
39
|
+
readonly changedAssets: readonly string[];
|
|
40
|
+
readonly narrative: string;
|
|
41
|
+
}
|
|
42
|
+
interface TimeMachineDoctorReport {
|
|
43
|
+
readonly capability: "time-machine";
|
|
44
|
+
readonly ok: boolean;
|
|
45
|
+
readonly checkedAt: string;
|
|
46
|
+
readonly truthSource: "event-log";
|
|
47
|
+
readonly features: readonly string[];
|
|
48
|
+
readonly suggestion?: string;
|
|
49
|
+
}
|
|
50
|
+
interface TimeMachineOptions {
|
|
51
|
+
readonly tenantId?: string;
|
|
52
|
+
readonly root?: string;
|
|
53
|
+
readonly debugRoot?: string;
|
|
54
|
+
readonly contentStore?: ContentStore;
|
|
55
|
+
readonly eventLog?: EventLog;
|
|
56
|
+
}
|
|
57
|
+
declare class TimeMachine {
|
|
58
|
+
#private;
|
|
59
|
+
private constructor();
|
|
60
|
+
static open(root?: string, options?: Omit<TimeMachineOptions, "root" | "contentStore" | "eventLog">): Promise<TimeMachine>;
|
|
61
|
+
static fromStores(options: Required<Pick<TimeMachineOptions, "tenantId" | "contentStore" | "eventLog">> & Pick<TimeMachineOptions, "root" | "debugRoot">): Promise<TimeMachine>;
|
|
62
|
+
timelineView(): Promise<TimelineView>;
|
|
63
|
+
branchFrom(eventId: string, name: string): Promise<TimelineBranch>;
|
|
64
|
+
star(eventId: string, label?: string): Promise<TimelineAnnotation>;
|
|
65
|
+
compare(leftEventId: string, rightEventId: string): Promise<TimelineComparison>;
|
|
66
|
+
rollbackDryRun(eventId: string): Promise<RollbackPlan>;
|
|
67
|
+
doctor(): Promise<TimeMachineDoctorReport>;
|
|
68
|
+
close(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
declare function readTimeMachineDebug(root?: string, limit?: number): Promise<unknown[]>;
|
|
71
|
+
|
|
72
|
+
export { TimeMachine, type TimeMachineDoctorReport, type TimeMachineOptions, type TimelineAnnotation, type TimelineBranch, type TimelineComparison, type TimelineNode, type TimelineNodeKind, type TimelineView, readTimeMachineDebug };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { EventLog } from "@nebutra/event-log";
|
|
2
|
+
import { TimeMachine } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const root = ".nebutra/time-machine-branch-example";
|
|
5
|
+
const tenantId = "local";
|
|
6
|
+
const eventLog = await EventLog.open(`${root}/event-log`, { tenantId });
|
|
7
|
+
const eventId = await eventLog.commit({
|
|
8
|
+
traceId: "demo_decision",
|
|
9
|
+
kind: "content_write",
|
|
10
|
+
affected: ["company/BRAND.md"],
|
|
11
|
+
parent: null,
|
|
12
|
+
snapshot: { "company/BRAND.md": "name: Loop\npalette: blue\n" },
|
|
13
|
+
});
|
|
14
|
+
const machine = await TimeMachine.open(root, { tenantId });
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
process.stdout.write(
|
|
18
|
+
`${JSON.stringify(await machine.branchFrom(eventId, "purple version"), null, 2)}\n`,
|
|
19
|
+
);
|
|
20
|
+
} finally {
|
|
21
|
+
await machine.close();
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TimeMachine } from "../src/index";
|
|
2
|
+
|
|
3
|
+
const machine = await TimeMachine.open(".nebutra/time-machine-example", { tenantId: "local" });
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
process.stdout.write(`${JSON.stringify(await machine.doctor(), null, 2)}\n`);
|
|
7
|
+
} finally {
|
|
8
|
+
await machine.close();
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TimeMachine } from "../src/index";
|
|
2
|
+
|
|
3
|
+
const machine = await TimeMachine.open(".nebutra/time-machine-example", { tenantId: "local" });
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const view = await machine.timelineView();
|
|
7
|
+
process.stdout.write(`${JSON.stringify(view, null, 2)}\n`);
|
|
8
|
+
} finally {
|
|
9
|
+
await machine.close();
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebutra/time-machine",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Chronos product surface for timeline browsing, branching, comparison, and rollback previews",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"nebutra": {
|
|
9
|
+
"status": "wip",
|
|
10
|
+
"productionReady": false,
|
|
11
|
+
"surface": "ecosystem-product",
|
|
12
|
+
"requires": [
|
|
13
|
+
"@nebutra/event-log for append-only truth",
|
|
14
|
+
"@nebutra/content-store for annotation artifacts",
|
|
15
|
+
"@nebutra/capability-kit for debug and doctor surfaces"
|
|
16
|
+
],
|
|
17
|
+
"gaps": [
|
|
18
|
+
"GUI star-map layout is represented as a data contract until the dashboard surface is wired",
|
|
19
|
+
"Rollback apply remains intentionally absent; only dry-run plans are exposed",
|
|
20
|
+
"Large binary media diffing is represented as metadata until sandbox diff workers are available"
|
|
21
|
+
],
|
|
22
|
+
"featureId": "time-machine",
|
|
23
|
+
"category": "ai",
|
|
24
|
+
"summary": "Founder-facing view over event-log history without owning event truth"
|
|
25
|
+
},
|
|
26
|
+
"main": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nebutra/capability-kit": "0.2.0",
|
|
33
|
+
"@nebutra/content-store": "0.1.0",
|
|
34
|
+
"@nebutra/errors": "0.1.0",
|
|
35
|
+
"@nebutra/event-log": "0.1.0",
|
|
36
|
+
"@nebutra/generation-context": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.19.15",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"tsx": "^4.21.0",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vitest": "^4.1.4"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/Nebutra/Nebutra-Sailor/tree/main/packages/ai/time-machine#readme",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Nebutra/Nebutra-Sailor.git",
|
|
49
|
+
"directory": "packages/ai/time-machine"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/Nebutra/Nebutra-Sailor/issues"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: timeline_view
|
|
3
|
+
kind: play
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: Build a founder-facing timeline view from event-log history
|
|
6
|
+
required_skills:
|
|
7
|
+
- event_log.timeline
|
|
8
|
+
- content_store.write
|
|
9
|
+
sub_agents:
|
|
10
|
+
- role: timeline_curator
|
|
11
|
+
allowed_skills: [event_log.timeline, content_store.write]
|
|
12
|
+
depends_on_plays: []
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What this play does
|
|
16
|
+
|
|
17
|
+
Reads Chronos event history and produces a founder-facing star-map timeline.
|
|
18
|
+
|
|
19
|
+
## Boundaries
|
|
20
|
+
|
|
21
|
+
- Event truth stays in event-log.
|
|
22
|
+
- Rollback is dry-run only until the user explicitly confirms a higher-layer apply flow.
|
|
23
|
+
- Branches are experiments, not destructive rewrites.
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readTimeMachineDebug, TimeMachine } from "./index";
|
|
2
|
+
|
|
3
|
+
const command = process.argv[2] ?? "doctor";
|
|
4
|
+
const root = process.env.TIME_MACHINE_ROOT ?? ".nebutra/time-machine";
|
|
5
|
+
const tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
|
|
6
|
+
|
|
7
|
+
if (command === "doctor") {
|
|
8
|
+
const machine = await TimeMachine.open(root, { tenantId });
|
|
9
|
+
try {
|
|
10
|
+
process.stdout.write(`${JSON.stringify(await machine.doctor(), null, 2)}\n`);
|
|
11
|
+
} finally {
|
|
12
|
+
await machine.close();
|
|
13
|
+
}
|
|
14
|
+
} else if (command === "quickstart") {
|
|
15
|
+
const machine = await TimeMachine.open(root, { tenantId });
|
|
16
|
+
try {
|
|
17
|
+
process.stdout.write(`${JSON.stringify(await machine.timelineView(), null, 2)}\n`);
|
|
18
|
+
} finally {
|
|
19
|
+
await machine.close();
|
|
20
|
+
}
|
|
21
|
+
} else if (command === "debug") {
|
|
22
|
+
process.stdout.write(
|
|
23
|
+
`${JSON.stringify({ capability: "time-machine", entries: await readTimeMachineDebug() }, null, 2)}\n`,
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
process.stderr.write(`Unknown time-machine command: ${command}\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ContentStore } from "@nebutra/content-store";
|
|
5
|
+
import { EventLog } from "@nebutra/event-log";
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { readTimeMachineDebug, TimeMachine } from "./index";
|
|
8
|
+
|
|
9
|
+
let root: string | undefined;
|
|
10
|
+
let machine: TimeMachine | undefined;
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
if (machine) await machine.close();
|
|
14
|
+
if (root) await rm(root, { recursive: true, force: true });
|
|
15
|
+
root = undefined;
|
|
16
|
+
machine = undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
async function seed(): Promise<{ runtime: TimeMachine; first: string; second: string }> {
|
|
20
|
+
root = await mkdtemp(join(tmpdir(), "time-machine-"));
|
|
21
|
+
const tenantId = "tenant_a";
|
|
22
|
+
const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
|
|
23
|
+
const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
|
|
24
|
+
const first = await eventLog.commit({
|
|
25
|
+
traceId: "brand",
|
|
26
|
+
kind: "content_write",
|
|
27
|
+
affected: ["company/BRAND.md"],
|
|
28
|
+
parent: null,
|
|
29
|
+
snapshot: { "company/BRAND.md": "name: Loop\npalette: blue\n" },
|
|
30
|
+
});
|
|
31
|
+
const second = await eventLog.commit({
|
|
32
|
+
traceId: "film",
|
|
33
|
+
kind: "content_write",
|
|
34
|
+
affected: ["company/film_60s.mp4", "company/BRAND.md"],
|
|
35
|
+
parent: first,
|
|
36
|
+
snapshot: { "company/BRAND.md": "name: Loop\npalette: purple\n" },
|
|
37
|
+
});
|
|
38
|
+
machine = await TimeMachine.fromStores({
|
|
39
|
+
tenantId,
|
|
40
|
+
root,
|
|
41
|
+
contentStore,
|
|
42
|
+
eventLog,
|
|
43
|
+
});
|
|
44
|
+
return { runtime: machine, first, second };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("time-machine", () => {
|
|
48
|
+
it("projects event-log records into a founder timeline without owning truth", async () => {
|
|
49
|
+
const { runtime, first, second } = await seed();
|
|
50
|
+
|
|
51
|
+
const view = await runtime.timelineView();
|
|
52
|
+
|
|
53
|
+
expect(view.nodes.map((node) => node.eventId)).toEqual([first, second]);
|
|
54
|
+
expect(view.nodes[0]).toMatchObject({
|
|
55
|
+
kind: "milestone",
|
|
56
|
+
branchId: "main",
|
|
57
|
+
affectedAssets: ["company/BRAND.md"],
|
|
58
|
+
});
|
|
59
|
+
expect(view.truthSource).toBe("event-log");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("branches, stars, compares, and keeps rollback as dry-run only", async () => {
|
|
63
|
+
const { runtime, first, second } = await seed();
|
|
64
|
+
|
|
65
|
+
const branch = await runtime.branchFrom(first, "purple version");
|
|
66
|
+
const annotation = await runtime.star(first, "first brand");
|
|
67
|
+
const comparison = await runtime.compare(first, second);
|
|
68
|
+
const rollback = await runtime.rollbackDryRun(first);
|
|
69
|
+
|
|
70
|
+
expect(branch).toMatchObject({ name: "purple version", fromEventId: first });
|
|
71
|
+
expect(annotation.path).toBe(`time-machine/annotations/${first}.json`);
|
|
72
|
+
expect(comparison.changedAssets).toContain("company/film_60s.mp4");
|
|
73
|
+
expect(comparison.narrative).toContain("company/BRAND.md");
|
|
74
|
+
expect(rollback.dryRun).toBe(true);
|
|
75
|
+
expect(rollback.suggestion).toContain("Review");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("records debug entries through capability-kit helpers", async () => {
|
|
79
|
+
const { runtime } = await seed();
|
|
80
|
+
|
|
81
|
+
await runtime.doctor();
|
|
82
|
+
|
|
83
|
+
await expect(readTimeMachineDebug(root)).resolves.toEqual(expect.any(Array));
|
|
84
|
+
});
|
|
85
|
+
});
|