zig: implement full GCC workspace in src/main.zig
Swadhin Biswas committed 7 months ago
30573eb
1 parent 2028438
commit 30573eb
1 file changed
+577 -0
Showing 1 changed file+577-0
▼ZIG/src/main.zig
+577-0
| @@ -0,0 +1,577 @@ | ||
1 | +//! contexa — Git-inspired context management for LLM agents. | |
2 | +//! COMMIT, BRANCH, MERGE, and CONTEXT over versioned memory. | |
3 | +//! | |
4 | +//! Paper: "Git Context Controller: Manage the Context of LLM-based Agents like Git" | |
5 | +//! arXiv:2508.00031 — Junde Wu et al., 2025 | |
6 | +//! | |
7 | +//! File system layout: | |
8 | +//! .GCC/ | |
9 | +//! ├── main.md # Global roadmap / planning artifact | |
10 | +//! └── branches/ | |
11 | +//! ├── main/ | |
12 | +//! │ ├── log.md # Continuous OTA trace (Observation-Thought-Action) | |
13 | +//! │ ├── commit.md # Milestone-level commit summaries | |
14 | +//! │ └── metadata.yaml # Branch intent, status, creation info | |
15 | +//! └── <branch>/ | |
16 | +//! └── ... | |
17 | + | |
18 | +const std = @import("std"); | |
19 | +const fs = std.fs; | |
20 | +const mem = std.mem; | |
21 | +const fmt = std.fmt; | |
22 | + | |
23 | +pub const MAIN_BRANCH = "main"; | |
24 | +pub const GCC_DIR = ".GCC"; | |
25 | + | |
26 | +// ------------------------------------------------------------------ // | |
27 | +// Data Models // | |
28 | +// ------------------------------------------------------------------ // | |
29 | + | |
30 | +/// A single Observation–Thought–Action cycle (logged to log.md). | |
31 | +/// The paper logs continuous OTA cycles as the agent executes. | |
32 | +pub const OTARecord = struct { | |
33 | + step: usize, | |
34 | + timestamp: []const u8, | |
35 | + observation: []const u8, | |
36 | + thought: []const u8, | |
37 | + action: []const u8, | |
38 | + | |
39 | + /// Write this record to the log.md format used by the paper. | |
40 | + pub fn writeMarkdown(self: OTARecord, writer: anytype) !void { | |
41 | + try writer.print( | |
42 | + "### Step {d} — {s}\n**Observation:** {s}\n\n**Thought:** {s}\n\n**Action:** {s}\n\n---\n", | |
43 | + .{ self.step, self.timestamp, self.observation, self.thought, self.action }, | |
44 | + ); | |
45 | + } | |
46 | +}; | |
47 | + | |
48 | +/// A commit checkpoint (paper §3.2). | |
49 | +/// Fields: Branch Purpose, Previous Progress Summary, This Commit's Contribution. | |
50 | +pub const CommitRecord = struct { | |
51 | + commit_id: []const u8, | |
52 | + branch_name: []const u8, | |
53 | + branch_purpose: []const u8, | |
54 | + previous_progress_summary: []const u8, | |
55 | + this_commit_contribution: []const u8, | |
56 | + timestamp: []const u8, | |
57 | + | |
58 | + pub fn writeMarkdown(self: CommitRecord, writer: anytype) !void { | |
59 | + try writer.print( | |
60 | + "## Commit `{s}`\n**Timestamp:** {s}\n\n**Branch Purpose:** {s}\n\n" ++ | |
61 | + "**Previous Progress Summary:** {s}\n\n" ++ | |
62 | + "**This Commit's Contribution:** {s}\n\n---\n", | |
63 | + .{ | |
64 | + self.commit_id, self.timestamp, | |
65 | + self.branch_purpose, self.previous_progress_summary, | |
66 | + self.this_commit_contribution, | |
67 | + }, | |
68 | + ); | |
69 | + } | |
70 | +}; | |
71 | + | |
72 | +/// Branch metadata written to metadata.yaml (paper §3.1). | |
73 | +pub const BranchMetadata = struct { | |
74 | + name: []const u8, | |
75 | + purpose: []const u8, | |
76 | + created_from: []const u8, | |
77 | + created_at: []const u8, | |
78 | + status: []const u8, // "active" | "merged" | "abandoned" | |
79 | + merged_into: ?[]const u8 = null, | |
80 | + merged_at: ?[]const u8 = null, | |
81 | + | |
82 | + pub fn writeYaml(self: BranchMetadata, writer: anytype) !void { | |
83 | + try writer.print( | |
84 | + "name: {s}\npurpose: {s}\ncreated_from: {s}\ncreated_at: {s}\nstatus: {s}\n", | |
85 | + .{ self.name, self.purpose, self.created_from, self.created_at, self.status }, | |
86 | + ); | |
87 | + if (self.merged_into) |mi| { | |
88 | + try writer.print("merged_into: {s}\n", .{mi}); | |
89 | + } else { | |
90 | + try writer.writeAll("merged_into: null\n"); | |
91 | + } | |
92 | + if (self.merged_at) |ma| { | |
93 | + try writer.print("merged_at: {s}\n", .{ma}); | |
94 | + } else { | |
95 | + try writer.writeAll("merged_at: null\n"); | |
96 | + } | |
97 | + } | |
98 | +}; | |
99 | + | |
100 | +// ------------------------------------------------------------------ // | |
101 | +// Workspace // | |
102 | +// ------------------------------------------------------------------ // | |
103 | + | |
104 | +/// Manages the .GCC/ directory structure for one agent project. | |
105 | +/// Implements the four GCC commands from arXiv:2508.00031v2. | |
106 | +pub const Workspace = struct { | |
107 | + allocator: mem.Allocator, | |
108 | + root: []const u8, | |
109 | + current_branch: []const u8, | |
110 | + | |
111 | + const Self = @This(); | |
112 | + | |
113 | + pub fn init(allocator: mem.Allocator, project_root: []const u8) Self { | |
114 | + return .{ | |
115 | + .allocator = allocator, | |
116 | + .root = project_root, | |
117 | + .current_branch = MAIN_BRANCH, | |
118 | + }; | |
119 | + } | |
120 | + | |
121 | + // ---------------------------------------------------------------- // | |
122 | + // Path helpers // | |
123 | + // ---------------------------------------------------------------- // | |
124 | + | |
125 | + fn gccPath(self: Self) ![]u8 { | |
126 | + return fs.path.join(self.allocator, &.{ self.root, GCC_DIR }); | |
127 | + } | |
128 | + | |
129 | + fn branchDir(self: Self, b: []const u8) ![]u8 { | |
130 | + return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b }); | |
131 | + } | |
132 | + | |
133 | + fn logPath(self: Self, b: []const u8) ![]u8 { | |
134 | + return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b, "log.md" }); | |
135 | + } | |
136 | + | |
137 | + fn commitFilePath(self: Self, b: []const u8) ![]u8 { | |
138 | + return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b, "commit.md" }); | |
139 | + } | |
140 | + | |
141 | + fn metaFilePath(self: Self, b: []const u8) ![]u8 { | |
142 | + return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "branches", b, "metadata.yaml" }); | |
143 | + } | |
144 | + | |
145 | + fn mainMdPath(self: Self) ![]u8 { | |
146 | + return fs.path.join(self.allocator, &.{ self.root, GCC_DIR, "main.md" }); | |
147 | + } | |
148 | + | |
149 | + // ---------------------------------------------------------------- // | |
150 | + // I/O helpers // | |
151 | + // ---------------------------------------------------------------- // | |
152 | + | |
153 | + fn writeFile(self: Self, path: []const u8, content: []const u8) !void { | |
154 | + _ = self; | |
155 | + const file = try fs.createFileAbsolute(path, .{ .truncate = true }); | |
156 | + defer file.close(); | |
157 | + try file.writeAll(content); | |
158 | + } | |
159 | + | |
160 | + fn appendToFile(self: Self, path: []const u8, content: []const u8) !void { | |
161 | + _ = self; | |
162 | + const file = try fs.openFileAbsolute(path, .{ .mode = .write_only }); | |
163 | + defer file.close(); | |
164 | + try file.seekFromEnd(0); | |
165 | + try file.writeAll(content); | |
166 | + } | |
167 | + | |
168 | + fn readFile(self: Self, path: []const u8) ![]u8 { | |
169 | + const file = fs.openFileAbsolute(path, .{}) catch return try self.allocator.dupe(u8, ""); | |
170 | + defer file.close(); | |
171 | + return file.readToEndAlloc(self.allocator, 10 * 1024 * 1024); | |
172 | + } | |
173 | + | |
174 | + fn countOTASteps(self: Self, b: []const u8) !usize { | |
175 | + const path = try self.logPath(b); | |
176 | + defer self.allocator.free(path); | |
177 | + const content = try self.readFile(path); | |
178 | + defer self.allocator.free(content); | |
179 | + var count: usize = 0; | |
180 | + var it = mem.splitSequence(u8, content, "### Step "); | |
181 | + _ = it.next(); // skip header | |
182 | + while (it.next()) |_| count += 1; | |
183 | + return count; | |
184 | + } | |
185 | + | |
186 | + fn countCommits(self: Self, b: []const u8) !usize { | |
187 | + const path = try self.commitFilePath(b); | |
188 | + defer self.allocator.free(path); | |
189 | + const content = try self.readFile(path); | |
190 | + defer self.allocator.free(content); | |
191 | + var count: usize = 0; | |
192 | + var it = mem.splitSequence(u8, content, "## Commit `"); | |
193 | + _ = it.next(); // skip header | |
194 | + while (it.next()) |_| count += 1; | |
195 | + return count; | |
196 | + } | |
197 | + | |
198 | + fn nowTimestamp(_: Self) []const u8 { | |
199 | + // Returns a static placeholder; in production use std.time | |
200 | + return "2025-01-01T00:00:00Z"; | |
201 | + } | |
202 | + | |
203 | + fn generateID(self: Self) ![]u8 { | |
204 | + var rng = std.Random.DefaultPrng.init(@intCast(std.time.milliTimestamp())); | |
205 | + const r = rng.random(); | |
206 | + const id = try fmt.allocPrint(self.allocator, "{x:0>8}", .{r.int(u32)}); | |
207 | + return id; | |
208 | + } | |
209 | + | |
210 | + // ---------------------------------------------------------------- // | |
211 | + // GCC Workspace Initialisation // | |
212 | + // ---------------------------------------------------------------- // | |
213 | + | |
214 | + /// Initialise a new GCC workspace. | |
215 | + /// Creates .GCC/ structure: main.md, branches/main/{log,commit,metadata}. | |
216 | + pub fn create(self: *Self, project_roadmap: []const u8) !void { | |
217 | + const gcc = try self.gccPath(); | |
218 | + defer self.allocator.free(gcc); | |
219 | + | |
220 | + // Create directory structure | |
221 | + const branch_dir = try self.branchDir(MAIN_BRANCH); | |
222 | + defer self.allocator.free(branch_dir); | |
223 | + try fs.makeDirAbsolute(gcc); | |
224 | + const branches_path = try fs.path.join(self.allocator, &.{ gcc, "branches" }); | |
225 | + defer self.allocator.free(branches_path); | |
226 | + try fs.makeDirAbsolute(branches_path); | |
227 | + try fs.makeDirAbsolute(branch_dir); | |
228 | + | |
229 | + // main.md — global roadmap | |
230 | + const main_path = try self.mainMdPath(); | |
231 | + defer self.allocator.free(main_path); | |
232 | + const roadmap = try fmt.allocPrint( | |
233 | + self.allocator, | |
234 | + "# Project Roadmap\n\n**Initialized:** {s}\n\n{s}\n", | |
235 | + .{ self.nowTimestamp(), project_roadmap }, | |
236 | + ); | |
237 | + defer self.allocator.free(roadmap); | |
238 | + try self.writeFile(main_path, roadmap); | |
239 | + | |
240 | + // log.md | |
241 | + const log = try self.logPath(MAIN_BRANCH); | |
242 | + defer self.allocator.free(log); | |
243 | + try self.writeFile(log, "# OTA Log — branch `main`\n\n"); | |
244 | + | |
245 | + // commit.md | |
246 | + const commit_file = try self.commitFilePath(MAIN_BRANCH); | |
247 | + defer self.allocator.free(commit_file); | |
248 | + try self.writeFile(commit_file, "# Commit History — branch `main`\n\n"); | |
249 | + | |
250 | + // metadata.yaml | |
251 | + const meta_file = try self.metaFilePath(MAIN_BRANCH); | |
252 | + defer self.allocator.free(meta_file); | |
253 | + var buf: std.ArrayListUnmanaged(u8) = .empty; | |
254 | + defer buf.deinit(self.allocator); | |
255 | + const meta = BranchMetadata{ | |
256 | + .name = MAIN_BRANCH, | |
257 | + .purpose = "Primary reasoning trajectory", | |
258 | + .created_from = "", | |
259 | + .created_at = self.nowTimestamp(), | |
260 | + .status = "active", | |
261 | + }; | |
262 | + try meta.writeYaml(buf.writer(self.allocator)); | |
263 | + try self.writeFile(meta_file, buf.items); | |
264 | + | |
265 | + self.current_branch = MAIN_BRANCH; | |
266 | + } | |
267 | + | |
268 | + // ---------------------------------------------------------------- // | |
269 | + // GCC Commands // | |
270 | + // ---------------------------------------------------------------- // | |
271 | + | |
272 | + /// Append an OTA step to current branch's log.md. | |
273 | + /// The paper logs continuous Observation–Thought–Action cycles. | |
274 | + pub fn logOTA( | |
275 | + self: *Self, | |
276 | + observation: []const u8, | |
277 | + thought: []const u8, | |
278 | + action: []const u8, | |
279 | + ) !OTARecord { | |
280 | + const step = (try self.countOTASteps(self.current_branch)) + 1; | |
281 | + const record = OTARecord{ | |
282 | + .step = step, | |
283 | + .timestamp = self.nowTimestamp(), | |
284 | + .observation = observation, | |
285 | + .thought = thought, | |
286 | + .action = action, | |
287 | + }; | |
288 | + const log = try self.logPath(self.current_branch); | |
289 | + defer self.allocator.free(log); | |
290 | + | |
291 | + var buf: std.ArrayListUnmanaged(u8) = .empty; | |
292 | + defer buf.deinit(self.allocator); | |
293 | + try record.writeMarkdown(buf.writer(self.allocator)); | |
294 | + try self.appendToFile(log, buf.items); | |
295 | + return record; | |
296 | + } | |
297 | + | |
298 | + /// COMMIT command (paper §3.2). | |
299 | + /// Checkpoints milestone: Branch Purpose, Previous Progress Summary, | |
300 | + /// This Commit's Contribution. | |
301 | + pub fn commit( | |
302 | + self: *Self, | |
303 | + contribution: []const u8, | |
304 | + previous_summary: ?[]const u8, | |
305 | + ) !CommitRecord { | |
306 | + const id = try self.generateID(); | |
307 | + | |
308 | + const prev = previous_summary orelse "Initial state — no prior commits."; | |
309 | + | |
310 | + const record = CommitRecord{ | |
311 | + .commit_id = id, | |
312 | + .branch_name = self.current_branch, | |
313 | + .branch_purpose = "Active branch", | |
314 | + .previous_progress_summary = prev, | |
315 | + .this_commit_contribution = contribution, | |
316 | + .timestamp = self.nowTimestamp(), | |
317 | + }; | |
318 | + | |
319 | + const commit_file = try self.commitFilePath(self.current_branch); | |
320 | + defer self.allocator.free(commit_file); | |
321 | + | |
322 | + var buf: std.ArrayListUnmanaged(u8) = .empty; | |
323 | + defer buf.deinit(self.allocator); | |
324 | + try record.writeMarkdown(buf.writer(self.allocator)); | |
325 | + try self.appendToFile(commit_file, buf.items); | |
326 | + | |
327 | + // Return record (commit_id is already owned by caller via generateID) | |
328 | + return CommitRecord{ | |
329 | + .commit_id = id, | |
330 | + .branch_name = self.current_branch, | |
331 | + .branch_purpose = "Active branch", | |
332 | + .previous_progress_summary = prev, | |
333 | + .this_commit_contribution = contribution, | |
334 | + .timestamp = self.nowTimestamp(), | |
335 | + }; | |
336 | + } | |
337 | + | |
338 | + /// BRANCH command (paper §3.3). | |
339 | + /// Creates isolated workspace: B_t^(name) = BRANCH(M_{t-1}). | |
340 | + pub fn branch(self: *Self, name: []const u8, purpose: []const u8) !void { | |
341 | + const branch_dir = try self.branchDir(name); | |
342 | + defer self.allocator.free(branch_dir); | |
343 | + try fs.makeDirAbsolute(branch_dir); | |
344 | + | |
345 | + // Empty OTA log (fresh execution trace, per paper §3.3) | |
346 | + const log = try self.logPath(name); | |
347 | + defer self.allocator.free(log); | |
348 | + const log_header = try fmt.allocPrint( | |
349 | + self.allocator, | |
350 | + "# OTA Log — branch `{s}`\n\n", | |
351 | + .{name}, | |
352 | + ); | |
353 | + defer self.allocator.free(log_header); | |
354 | + try self.writeFile(log, log_header); | |
355 | + | |
356 | + // Empty commit.md | |
357 | + const commit_file = try self.commitFilePath(name); | |
358 | + defer self.allocator.free(commit_file); | |
359 | + const commit_header = try fmt.allocPrint( | |
360 | + self.allocator, | |
361 | + "# Commit History — branch `{s}`\n\n", | |
362 | + .{name}, | |
363 | + ); | |
364 | + defer self.allocator.free(commit_header); | |
365 | + try self.writeFile(commit_file, commit_header); | |
366 | + | |
367 | + // metadata.yaml records intent and motivation (paper §3.3) | |
368 | + const meta_file = try self.metaFilePath(name); | |
369 | + defer self.allocator.free(meta_file); | |
370 | + var buf: std.ArrayListUnmanaged(u8) = .empty; | |
371 | + defer buf.deinit(self.allocator); | |
372 | + const meta = BranchMetadata{ | |
373 | + .name = name, | |
374 | + .purpose = purpose, | |
375 | + .created_from = self.current_branch, | |
376 | + .created_at = self.nowTimestamp(), | |
377 | + .status = "active", | |
378 | + }; | |
379 | + try meta.writeYaml(buf.writer(self.allocator)); | |
380 | + try self.writeFile(meta_file, buf.items); | |
381 | + | |
382 | + self.current_branch = name; | |
383 | + } | |
384 | + | |
385 | + /// MERGE command (paper §3.4). | |
386 | + /// Integrates branch into target, merging summaries and OTA traces. | |
387 | + pub fn merge(self: *Self, branch_name: []const u8, target: []const u8) !CommitRecord { | |
388 | + // Append branch OTA to target's log | |
389 | + const src_log = try self.logPath(branch_name); | |
390 | + defer self.allocator.free(src_log); | |
391 | + const src_content = try self.readFile(src_log); | |
392 | + defer self.allocator.free(src_content); | |
393 | + | |
394 | + if (src_content.len > 0) { | |
395 | + const target_log = try self.logPath(target); | |
396 | + defer self.allocator.free(target_log); | |
397 | + const header = try fmt.allocPrint( | |
398 | + self.allocator, | |
399 | + "\n## Merged from `{s}` ({s})\n\n", | |
400 | + .{ branch_name, self.nowTimestamp() }, | |
401 | + ); | |
402 | + defer self.allocator.free(header); | |
403 | + try self.appendToFile(target_log, header); | |
404 | + try self.appendToFile(target_log, src_content); | |
405 | + } | |
406 | + | |
407 | + // Switch to target and commit the merge | |
408 | + self.current_branch = target; | |
409 | + const summary = try fmt.allocPrint( | |
410 | + self.allocator, | |
411 | + "Merged branch `{s}` into `{s}`", | |
412 | + .{ branch_name, target }, | |
413 | + ); | |
414 | + // Note: summary is owned by allocator; commit will reference it, then we free | |
415 | + const merge_commit = try self.commit(summary, null); | |
416 | + | |
417 | + // Update branch metadata to mark as merged | |
418 | + const meta_file = try self.metaFilePath(branch_name); | |
419 | + defer self.allocator.free(meta_file); | |
420 | + var buf: std.ArrayListUnmanaged(u8) = .empty; | |
421 | + defer buf.deinit(self.allocator); | |
422 | + const meta = BranchMetadata{ | |
423 | + .name = branch_name, | |
424 | + .purpose = "Merged branch", | |
425 | + .created_from = "", | |
426 | + .created_at = self.nowTimestamp(), | |
427 | + .status = "merged", | |
428 | + .merged_into = target, | |
429 | + .merged_at = self.nowTimestamp(), | |
430 | + }; | |
431 | + try meta.writeYaml(buf.writer(self.allocator)); | |
432 | + try self.writeFile(meta_file, buf.items); | |
433 | + | |
434 | + return merge_commit; | |
435 | + } | |
436 | + | |
437 | + /// CONTEXT command (paper §3.5). | |
438 | + /// Reads the K most-recent commits + OTA log from the given branch. | |
439 | + /// Paper experiments fix K=1 (most recent commit record only). | |
440 | + pub fn context(self: Self, branch_name: ?[]const u8, k: usize) !ContextSnapshot { | |
441 | + const target = branch_name orelse self.current_branch; | |
442 | + | |
443 | + const main_path = try self.mainMdPath(); | |
444 | + defer self.allocator.free(main_path); | |
445 | + const roadmap = try self.readFile(main_path); | |
446 | + | |
447 | + const log = try self.logPath(target); | |
448 | + defer self.allocator.free(log); | |
449 | + const ota_content = try self.readFile(log); | |
450 | + | |
451 | + const commit_file = try self.commitFilePath(target); | |
452 | + defer self.allocator.free(commit_file); | |
453 | + const commit_content = try self.readFile(commit_file); | |
454 | + | |
455 | + return ContextSnapshot{ | |
456 | + .branch_name = target, | |
457 | + .k = k, | |
458 | + .main_roadmap = roadmap, | |
459 | + .ota_log = ota_content, | |
460 | + .commit_history = commit_content, | |
461 | + }; | |
462 | + } | |
463 | + | |
464 | + pub fn currentBranch(self: Self) []const u8 { | |
465 | + return self.current_branch; | |
466 | + } | |
467 | + | |
468 | + pub fn switchBranch(self: *Self, name: []const u8) void { | |
469 | + self.current_branch = name; | |
470 | + } | |
471 | +}; | |
472 | + | |
473 | +/// Snapshot returned by the CONTEXT command (paper §3.5). | |
474 | +/// The caller is responsible for freeing main_roadmap, ota_log, and commit_history. | |
475 | +pub const ContextSnapshot = struct { | |
476 | + branch_name: []const u8, | |
477 | + k: usize, | |
478 | + main_roadmap: []const u8, | |
479 | + ota_log: []const u8, | |
480 | + commit_history: []const u8, | |
481 | + | |
482 | + /// Free all owned memory. | |
483 | + pub fn deinit(self: *const ContextSnapshot, allocator: mem.Allocator) void { | |
484 | + allocator.free(self.main_roadmap); | |
485 | + allocator.free(self.ota_log); | |
486 | + allocator.free(self.commit_history); | |
487 | + } | |
488 | +}; | |
489 | + | |
490 | +// ------------------------------------------------------------------ // | |
491 | +// Tests // | |
492 | +// ------------------------------------------------------------------ // | |
493 | + | |
494 | +test "workspace init creates GCC structure" { | |
495 | + var tmp = std.testing.tmpDir(.{}); | |
496 | + defer tmp.cleanup(); | |
497 | + | |
498 | + const root = try tmp.dir.realpathAlloc(std.testing.allocator, "."); | |
499 | + defer std.testing.allocator.free(root); | |
500 | + | |
501 | + var ws = Workspace.init(std.testing.allocator, root); | |
502 | + try ws.create("Test project"); | |
503 | + | |
504 | + // Verify .GCC/main.md exists | |
505 | + const main_path = try ws.mainMdPath(); | |
506 | + defer std.testing.allocator.free(main_path); | |
507 | + const f = try fs.openFileAbsolute(main_path, .{}); | |
508 | + f.close(); | |
509 | +} | |
510 | + | |
511 | +test "logOTA increments step" { | |
512 | + var tmp = std.testing.tmpDir(.{}); | |
513 | + defer tmp.cleanup(); | |
514 | + | |
515 | + const root = try tmp.dir.realpathAlloc(std.testing.allocator, "."); | |
516 | + defer std.testing.allocator.free(root); | |
517 | + | |
518 | + var ws = Workspace.init(std.testing.allocator, root); | |
519 | + try ws.create("Test"); | |
520 | + | |
521 | + const r1 = try ws.logOTA("obs1", "thought1", "action1"); | |
522 | + const r2 = try ws.logOTA("obs2", "thought2", "action2"); | |
523 | + try std.testing.expectEqual(@as(usize, 1), r1.step); | |
524 | + try std.testing.expectEqual(@as(usize, 2), r2.step); | |
525 | +} | |
526 | + | |
527 | +test "commit writes checkpoint" { | |
528 | + var tmp = std.testing.tmpDir(.{}); | |
529 | + defer tmp.cleanup(); | |
530 | + | |
531 | + const root = try tmp.dir.realpathAlloc(std.testing.allocator, "."); | |
532 | + defer std.testing.allocator.free(root); | |
533 | + | |
534 | + var ws = Workspace.init(std.testing.allocator, root); | |
535 | + try ws.create("Test"); | |
536 | + | |
537 | + const c = try ws.commit("Initial scaffold done", null); | |
538 | + defer std.testing.allocator.free(c.commit_id); | |
539 | + try std.testing.expectEqualStrings("Initial scaffold done", c.this_commit_contribution); | |
540 | + try std.testing.expectEqualStrings(MAIN_BRANCH, c.branch_name); | |
541 | +} | |
542 | + | |
543 | +test "branch creates isolated workspace" { | |
544 | + var tmp = std.testing.tmpDir(.{}); | |
545 | + defer tmp.cleanup(); | |
546 | + | |
547 | + const root = try tmp.dir.realpathAlloc(std.testing.allocator, "."); | |
548 | + defer std.testing.allocator.free(root); | |
549 | + | |
550 | + var ws = Workspace.init(std.testing.allocator, root); | |
551 | + try ws.create("Test"); | |
552 | + try ws.branch("experiment", "Try alternative"); | |
553 | + try std.testing.expectEqualStrings("experiment", ws.currentBranch()); | |
554 | + | |
555 | + // Branch should have fresh empty OTA log | |
556 | + const step_count = try ws.countOTASteps("experiment"); | |
557 | + try std.testing.expectEqual(@as(usize, 0), step_count); | |
558 | +} | |
559 | + | |
560 | +test "context returns roadmap and history" { | |
561 | + var tmp = std.testing.tmpDir(.{}); | |
562 | + defer tmp.cleanup(); | |
563 | + | |
564 | + const root = try tmp.dir.realpathAlloc(std.testing.allocator, "."); | |
565 | + defer std.testing.allocator.free(root); | |
566 | + | |
567 | + var ws = Workspace.init(std.testing.allocator, root); | |
568 | + try ws.create("My AI project roadmap"); | |
569 | + const c = try ws.commit("First milestone", null); | |
570 | + defer std.testing.allocator.free(c.commit_id); | |
571 | + | |
572 | + const ctx = try ws.context(null, 1); | |
573 | + defer ctx.deinit(std.testing.allocator); | |
574 | + | |
575 | + try std.testing.expect(mem.indexOf(u8, ctx.main_roadmap, "My AI project roadmap") != null); | |
576 | + try std.testing.expect(mem.indexOf(u8, ctx.commit_history, "First milestone") != null); | |
577 | +} | |
0 | 578 | |