parallax-opencode 0.3.7 → 0.3.9

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/dist/plugin.js CHANGED
@@ -96,7 +96,45 @@ function loadConfig() {
96
96
  // State persistence (Phase 2.1)
97
97
  // ---------------------------------------------------------------------------
98
98
  let stateDebounceTimer = null;
99
- function writeState() {
99
+ function flushState() {
100
+ try {
101
+ const s = getFriction();
102
+ const m = getMode();
103
+ const p = getProtocol();
104
+ const trace = getTrace(sessionId());
105
+ const state = {
106
+ sessionId: "current",
107
+ sessionStart: trace.session.startedAt,
108
+ mode: m.mode,
109
+ friction: {
110
+ successes: s.successes,
111
+ trials: s.trials,
112
+ retriesLeft: s.retriesLeft,
113
+ lastObservation: s.lastObservation,
114
+ },
115
+ protocol: {
116
+ ambiguityDone: p.ambiguityDone,
117
+ invariantsDone: p.invariantsDone,
118
+ gateDone: p.gateDone,
119
+ designDone: p.designDone,
120
+ commitDone: p.commitDone,
121
+ summaryDone: p.summaryDone,
122
+ writesBeforeGate: p.writesBeforeGate,
123
+ gateBlocked: p.gateBlocked,
124
+ },
125
+ };
126
+ // Debug: write BEFORE state file to isolate error
127
+ const json = JSON.stringify(state, null, 2);
128
+ writeFileSync(STATE_FILE, json, "utf8");
129
+ }
130
+ catch {
131
+ }
132
+ }
133
+ function writeState(immediate = false) {
134
+ if (immediate) {
135
+ flushState();
136
+ return;
137
+ }
100
138
  if (stateDebounceTimer)
101
139
  clearTimeout(stateDebounceTimer);
102
140
  stateDebounceTimer = setTimeout(() => {
@@ -109,7 +147,7 @@ function writeState() {
109
147
  const diskState = readProtocolFromDisk();
110
148
  const p = diskState || getProtocol();
111
149
  const state = {
112
- sessionId: currentSessionId,
150
+ sessionId: "current",
113
151
  sessionStart: getTrace(sessionId()).session.startedAt,
114
152
  mode: m.mode,
115
153
  friction: {
@@ -286,7 +324,7 @@ export default {
286
324
  if (step === "ambiguity" && !p.ambiguityDone) {
287
325
  p.ambiguityDone = true;
288
326
  addPhase(sid, "ambiguity_check");
289
- writeState();
327
+ writeState(true);
290
328
  return "[parallax] Step 1/6: Ambiguity Check marked complete.";
291
329
  }
292
330
  if (step === "invariants") {
@@ -295,7 +333,7 @@ export default {
295
333
  }
296
334
  p.invariantsDone = true;
297
335
  addPhase(sid, "four_invariants");
298
- writeState();
336
+ writeState(true);
299
337
  return "[parallax] Step 2/6: 4 Invariants marked complete.";
300
338
  }
301
339
  if (step === "gate") {
@@ -304,7 +342,7 @@ export default {
304
342
  }
305
343
  p.gateDone = true;
306
344
  addPhase(sid, "verification_gate");
307
- writeState();
345
+ writeState(true);
308
346
  return "[parallax] Step 3/6: Verification Gate marked complete.";
309
347
  }
310
348
  if (step === "design") {
@@ -313,19 +351,19 @@ export default {
313
351
  }
314
352
  p.designDone = true;
315
353
  addPhase(sid, "design_check");
316
- writeState();
354
+ writeState(true);
317
355
  return "[parallax] Step 4/6: Design Doc marked complete.";
318
356
  }
319
357
  if (step === "commit") {
320
358
  p.commitDone = true;
321
359
  addPhase(sid, "commit_decision");
322
- writeState();
360
+ writeState(true);
323
361
  return "[parallax] Step 5/6: Commit Decision marked complete.";
324
362
  }
325
363
  if (step === "summary") {
326
364
  p.summaryDone = true;
327
365
  addPhase(sid, "summary");
328
- writeState();
366
+ writeState(true);
329
367
  // Phase 2.3: Post-session retrospective
330
368
  const trace = getTrace(sid);
331
369
  const breakdown = computeCoherenceScore(trace);
@@ -563,7 +601,9 @@ export default {
563
601
  "tool.execute.before": async (input) => {
564
602
  if (!["write", "edit", "apply_patch"].includes(input.tool))
565
603
  return;
566
- const p = getProtocol();
604
+ // Read from disk: OpenCode loads plugin in separate execution contexts
605
+ // for tools vs hooks. In-memory Maps are NOT shared across contexts.
606
+ const p = readProtocolFromDisk() || getProtocol();
567
607
  const cfg = loadConfig();
568
608
  // Enforce ambiguity check before any write
569
609
  if (!p.ambiguityDone) {
@@ -287,7 +287,43 @@ function loadConfig() {
287
287
  return configCache || {};
288
288
  }
289
289
  var stateDebounceTimer = null;
290
- function writeState() {
290
+ function flushState() {
291
+ try {
292
+ const s = getFriction();
293
+ const m = getMode();
294
+ const p = getProtocol();
295
+ const trace = getTrace(sessionId());
296
+ const state = {
297
+ sessionId: "current",
298
+ sessionStart: trace.session.startedAt,
299
+ mode: m.mode,
300
+ friction: {
301
+ successes: s.successes,
302
+ trials: s.trials,
303
+ retriesLeft: s.retriesLeft,
304
+ lastObservation: s.lastObservation
305
+ },
306
+ protocol: {
307
+ ambiguityDone: p.ambiguityDone,
308
+ invariantsDone: p.invariantsDone,
309
+ gateDone: p.gateDone,
310
+ designDone: p.designDone,
311
+ commitDone: p.commitDone,
312
+ summaryDone: p.summaryDone,
313
+ writesBeforeGate: p.writesBeforeGate,
314
+ gateBlocked: p.gateBlocked
315
+ }
316
+ };
317
+ const json = JSON.stringify(state, null, 2);
318
+ writeFileSync2(STATE_FILE, json, "utf8");
319
+ } catch {
320
+ }
321
+ }
322
+ function writeState(immediate = false) {
323
+ if (immediate) {
324
+ flushState();
325
+ return;
326
+ }
291
327
  if (stateDebounceTimer) clearTimeout(stateDebounceTimer);
292
328
  stateDebounceTimer = setTimeout(() => {
293
329
  stateDebounceTimer = null;
@@ -297,7 +333,7 @@ function writeState() {
297
333
  const diskState = readProtocolFromDisk();
298
334
  const p = diskState || getProtocol();
299
335
  const state = {
300
- sessionId: currentSessionId,
336
+ sessionId: "current",
301
337
  sessionStart: getTrace(sessionId()).session.startedAt,
302
338
  mode: m.mode,
303
339
  friction: {
@@ -455,7 +491,7 @@ Use grep and read to investigate ${args.topic} in the codebase, then proceed wit
455
491
  if (step === "ambiguity" && !p.ambiguityDone) {
456
492
  p.ambiguityDone = true;
457
493
  addPhase(sid, "ambiguity_check");
458
- writeState();
494
+ writeState(true);
459
495
  return "[parallax] Step 1/6: Ambiguity Check marked complete.";
460
496
  }
461
497
  if (step === "invariants") {
@@ -464,7 +500,7 @@ Use grep and read to investigate ${args.topic} in the codebase, then proceed wit
464
500
  }
465
501
  p.invariantsDone = true;
466
502
  addPhase(sid, "four_invariants");
467
- writeState();
503
+ writeState(true);
468
504
  return "[parallax] Step 2/6: 4 Invariants marked complete.";
469
505
  }
470
506
  if (step === "gate") {
@@ -473,7 +509,7 @@ Use grep and read to investigate ${args.topic} in the codebase, then proceed wit
473
509
  }
474
510
  p.gateDone = true;
475
511
  addPhase(sid, "verification_gate");
476
- writeState();
512
+ writeState(true);
477
513
  return "[parallax] Step 3/6: Verification Gate marked complete.";
478
514
  }
479
515
  if (step === "design") {
@@ -482,19 +518,19 @@ Use grep and read to investigate ${args.topic} in the codebase, then proceed wit
482
518
  }
483
519
  p.designDone = true;
484
520
  addPhase(sid, "design_check");
485
- writeState();
521
+ writeState(true);
486
522
  return "[parallax] Step 4/6: Design Doc marked complete.";
487
523
  }
488
524
  if (step === "commit") {
489
525
  p.commitDone = true;
490
526
  addPhase(sid, "commit_decision");
491
- writeState();
527
+ writeState(true);
492
528
  return "[parallax] Step 5/6: Commit Decision marked complete.";
493
529
  }
494
530
  if (step === "summary") {
495
531
  p.summaryDone = true;
496
532
  addPhase(sid, "summary");
497
- writeState();
533
+ writeState(true);
498
534
  const trace = getTrace(sid);
499
535
  const breakdown = computeCoherenceScore(trace);
500
536
  const s = getFriction();
@@ -695,7 +731,7 @@ Coherence Score: ${breakdown.total}/100`;
695
731
  // -----------------------------------------------------------------------
696
732
  "tool.execute.before": async (input) => {
697
733
  if (!["write", "edit", "apply_patch"].includes(input.tool)) return;
698
- const p = getProtocol();
734
+ const p = readProtocolFromDisk() || getProtocol();
699
735
  const cfg = loadConfig();
700
736
  if (!p.ambiguityDone) {
701
737
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallax-opencode",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "PARALLAX ENGINE plugin for OpenCode -- protocol enforcement, friction-loop verification, mode switching (plan/build/debug), trace recording, coherence scoring, CI gate, and PR-ready trace artifacts",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -1 +0,0 @@
1
- export {};
@@ -1,75 +0,0 @@
1
- /**
2
- * Tests for project detection logic.
3
- */
4
- import { describe, it, expect, vi, beforeEach } from "vitest";
5
- // Mock fs functions used by detectProject
6
- vi.mock("fs", () => ({
7
- existsSync: vi.fn(),
8
- statSync: vi.fn(),
9
- readFileSync: vi.fn(),
10
- }));
11
- function testDetect(files, dirs) {
12
- const mockExists = vi.fn((p) => {
13
- const path = typeof p === "string" ? p : String(p);
14
- return files[path] === true;
15
- });
16
- const mockStat = vi.fn((p) => {
17
- const path = typeof p === "string" ? p : String(p);
18
- if (dirs[path])
19
- return { isDirectory: () => true };
20
- return { isDirectory: () => false };
21
- });
22
- // Inline the detection logic from plugin.ts
23
- try {
24
- if (mockExists("Cargo.toml"))
25
- return "cargo";
26
- if (mockExists("package.json")) {
27
- if (mockExists("node_modules") && mockStat("node_modules").isDirectory()) {
28
- if (mockExists("tsconfig.json"))
29
- return "tsc";
30
- return "lint";
31
- }
32
- }
33
- if (mockExists("pyproject.toml") || mockExists("requirements.txt"))
34
- return "python";
35
- return null;
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
- describe("Project detection", () => {
42
- beforeEach(() => {
43
- vi.clearAllMocks();
44
- });
45
- it("detects cargo project", () => {
46
- const result = testDetect({ "Cargo.toml": true }, {});
47
- expect(result).toBe("cargo");
48
- });
49
- it("detects TypeScript project (tsconfig + node_modules)", () => {
50
- const result = testDetect({ "package.json": true, "node_modules": true, "tsconfig.json": true }, { "node_modules": true });
51
- expect(result).toBe("tsc");
52
- });
53
- it("detects JS/lint project (package.json + node_modules, no tsconfig)", () => {
54
- const result = testDetect({ "package.json": true, "node_modules": true }, { "node_modules": true });
55
- expect(result).toBe("lint");
56
- });
57
- it("detects Python project (pyproject.toml)", () => {
58
- const result = testDetect({ "pyproject.toml": true }, {});
59
- expect(result).toBe("python");
60
- });
61
- it("detects Python project (requirements.txt)", () => {
62
- const result = testDetect({ "requirements.txt": true }, {});
63
- expect(result).toBe("python");
64
- });
65
- it("returns null for unknown project", () => {
66
- const result = testDetect({}, {});
67
- expect(result).toBeNull();
68
- });
69
- it("handles errors gracefully", () => {
70
- // The try/catch should return null on any exception
71
- const result = testDetect({ "Cargo.toml": true }, {});
72
- // Should be "cargo" since we're not throwing
73
- expect(result).toBe("cargo");
74
- });
75
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,87 +0,0 @@
1
- /**
2
- * Tests for friction loop state machine.
3
- */
4
- import { describe, it, expect, beforeEach, vi } from "vitest";
5
- const MAX_FRICTION_RETRIES = 3;
6
- function createState() {
7
- return {
8
- successes: 0,
9
- trials: 0,
10
- retriesLeft: MAX_FRICTION_RETRIES,
11
- lastObservation: null,
12
- };
13
- }
14
- function recordSuccess(state) {
15
- state.successes++;
16
- state.trials++;
17
- state.retriesLeft = MAX_FRICTION_RETRIES;
18
- state.lastObservation = null;
19
- }
20
- function recordFailure(state, msg) {
21
- state.trials++;
22
- state.retriesLeft--;
23
- state.lastObservation = msg;
24
- }
25
- // Helper for debounce-like behavior
26
- function simulateTimeout() {
27
- vi.advanceTimersByTime(1000);
28
- }
29
- describe("Friction loop state machine", () => {
30
- let state;
31
- beforeEach(() => {
32
- state = createState();
33
- vi.useFakeTimers();
34
- });
35
- it("starts with full retries and no observation", () => {
36
- expect(state.successes).toBe(0);
37
- expect(state.trials).toBe(0);
38
- expect(state.retriesLeft).toBe(3);
39
- expect(state.lastObservation).toBeNull();
40
- });
41
- it("resets retries on success", () => {
42
- recordSuccess(state);
43
- expect(state.successes).toBe(1);
44
- expect(state.trials).toBe(1);
45
- expect(state.retriesLeft).toBe(3);
46
- expect(state.lastObservation).toBeNull();
47
- });
48
- it("decrements retries on failure", () => {
49
- recordFailure(state, "error: syntax error");
50
- expect(state.successes).toBe(0);
51
- expect(state.trials).toBe(1);
52
- expect(state.retriesLeft).toBe(2);
53
- expect(state.lastObservation).toBe("error: syntax error");
54
- });
55
- it("blocks after 3 consecutive failures", () => {
56
- recordFailure(state, "fail 1");
57
- expect(state.retriesLeft).toBe(2);
58
- recordFailure(state, "fail 2");
59
- expect(state.retriesLeft).toBe(1);
60
- recordFailure(state, "fail 3");
61
- expect(state.retriesLeft).toBe(0);
62
- expect(state.lastObservation).toBe("fail 3");
63
- });
64
- it("recovers after success following failures", () => {
65
- recordFailure(state, "fail 1");
66
- recordFailure(state, "fail 2");
67
- expect(state.retriesLeft).toBe(1);
68
- recordSuccess(state);
69
- expect(state.retriesLeft).toBe(3);
70
- expect(state.successes).toBe(1);
71
- });
72
- it("accumulates trials correctly", () => {
73
- recordSuccess(state);
74
- recordSuccess(state);
75
- recordFailure(state, "an error");
76
- recordSuccess(state);
77
- expect(state.trials).toBe(4);
78
- expect(state.successes).toBe(3);
79
- });
80
- it("isolates state per session", () => {
81
- const state2 = createState();
82
- recordFailure(state, "session A error");
83
- expect(state.retriesLeft).toBe(2);
84
- expect(state2.retriesLeft).toBe(3);
85
- expect(state2.lastObservation).toBeNull();
86
- });
87
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,80 +0,0 @@
1
- /**
2
- * Tests for protocol step ordering and enforcement.
3
- */
4
- import { describe, it, expect, beforeEach } from "vitest";
5
- const STEP_LABELS = {
6
- ambiguity: "Ambiguity Check",
7
- invariants: "4 Invariants",
8
- gate: "Verification Gate",
9
- commit: "Commit Decision",
10
- summary: "Summarize",
11
- };
12
- function createProtocol() {
13
- return {
14
- ambiguityDone: false,
15
- invariantsDone: false,
16
- gateDone: false,
17
- commitDone: false,
18
- summaryDone: false,
19
- writesBeforeGate: 0,
20
- gateBlocked: false,
21
- };
22
- }
23
- function checkin(p, step) {
24
- if (step === "ambiguity" && !p.ambiguityDone) {
25
- p.ambiguityDone = true;
26
- return "[parallax] Step 1/6: Ambiguity Check marked complete.";
27
- }
28
- if (step === "invariants") {
29
- if (!p.ambiguityDone)
30
- return "[parallax] ERROR: Complete Ambiguity Check first (Step 1).";
31
- p.invariantsDone = true;
32
- return "[parallax] Step 2/6: 4 Invariants marked complete.";
33
- }
34
- if (step === "gate") {
35
- if (!p.invariantsDone)
36
- return "[parallax] ERROR: Complete 4 Invariants first (Step 2).";
37
- p.gateDone = true;
38
- return "[parallax] Step 3/6: Verification Gate marked complete.";
39
- }
40
- if (step === "commit") {
41
- p.commitDone = true;
42
- return "[parallax] Step 5/6: Commit Decision marked complete.";
43
- }
44
- if (step === "summary") {
45
- p.summaryDone = true;
46
- return "[parallax] Step 6/6: Summary marked complete. Protocol finished.";
47
- }
48
- return `[parallax] Unknown step "${step}".`;
49
- }
50
- describe("Protocol step enforcement", () => {
51
- let p;
52
- beforeEach(() => {
53
- p = createProtocol();
54
- });
55
- it("enforces correct order: ambiguity -> invariants -> gate", () => {
56
- expect(checkin(p, "ambiguity")).toContain("Step 1/6");
57
- expect(checkin(p, "invariants")).toContain("Step 2/6");
58
- expect(checkin(p, "gate")).toContain("Step 3/6");
59
- });
60
- it("blocks invariants before ambiguity", () => {
61
- expect(checkin(p, "invariants")).toContain("ERROR");
62
- expect(p.invariantsDone).toBe(false);
63
- });
64
- it("blocks gate before invariants", () => {
65
- checkin(p, "ambiguity");
66
- expect(checkin(p, "gate")).toContain("ERROR");
67
- expect(p.gateDone).toBe(false);
68
- });
69
- it("allows commit and summary at any time after gate", () => {
70
- checkin(p, "ambiguity");
71
- checkin(p, "invariants");
72
- checkin(p, "gate");
73
- expect(checkin(p, "commit")).toContain("Step 5/6");
74
- expect(checkin(p, "summary")).toContain("Step 6/6");
75
- expect(p.summaryDone).toBe(true);
76
- });
77
- it("rejects unknown steps", () => {
78
- expect(checkin(p, "bogus")).toContain("Unknown step");
79
- });
80
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,106 +0,0 @@
1
- /**
2
- * Tests for coherence score computation.
3
- */
4
- import { describe, it, expect } from "vitest";
5
- function computeCoherenceScore(trace) {
6
- // 1. Protocol Coverage (30 points max)
7
- const required = ["ambiguity_check", "four_invariants", "verification_gate", "commit_decision", "summary"];
8
- const phaseNames = new Set(trace.phases.map((p) => p.phase));
9
- const completed = required.filter((r) => phaseNames.has(r)).length;
10
- const protocolScore = (completed / required.length) * 30;
11
- // 2. Verification Integrity (35 points max)
12
- let integrityScore = 0;
13
- if (trace.writes.length > 0) {
14
- const firstPass = trace.writes.filter((w) => w.verification === "pass" && w.frictionRetriesLeft === 3).length;
15
- integrityScore = (firstPass / trace.writes.length) * 35;
16
- }
17
- // 3. Edge Case Coverage (20 points max)
18
- // For now, check if analyze phases recorded edge categories
19
- const analyzePhases = trace.phases.filter((p) => p.phase === "mode_switch" && p.data.analysisTopic);
20
- const edgeScore = Math.min(analyzePhases.length / 3, 1) * 20;
21
- // 4. Timing Discipline (15 points max)
22
- const order = ["ambiguity_check", "four_invariants", "verification_gate", "commit_decision", "summary"];
23
- let inOrder = 0;
24
- let lastIdx = -1;
25
- for (const phase of trace.phases) {
26
- const idx = order.indexOf(phase.phase);
27
- if (idx > lastIdx) {
28
- inOrder++;
29
- lastIdx = idx;
30
- }
31
- }
32
- // Normalize to 5 possible
33
- const timingScore = Math.min(inOrder / order.length, 1) * 15;
34
- return Math.round(protocolScore + integrityScore + edgeScore + timingScore);
35
- }
36
- function makeTrace(phases, writes) {
37
- return {
38
- schemaVersion: "1.0",
39
- session: {
40
- id: "test",
41
- agent: "parallax",
42
- agentVersion: "0.2.0",
43
- startedAt: "2026-01-01T00:00:00.000Z",
44
- endedAt: "2026-01-01T01:00:00.000Z",
45
- },
46
- phases: phases.map((p) => ({
47
- phase: p,
48
- timestamp: "2026-01-01T00:00:00.000Z",
49
- data: {},
50
- })),
51
- writes,
52
- coherenceScore: null,
53
- };
54
- }
55
- describe("Coherence score", () => {
56
- it("returns 0 for empty trace", () => {
57
- const trace = makeTrace([], []);
58
- const score = computeCoherenceScore(trace);
59
- expect(score).toBe(0);
60
- });
61
- it("returns near-perfect for complete protocol with all passes", () => {
62
- const trace = makeTrace(["ambiguity_check", "four_invariants", "verification_gate", "commit_decision", "summary"], [
63
- { file: "a.ts", timestamp: "", verification: "pass", frictionRetriesLeft: 3 },
64
- { file: "b.ts", timestamp: "", verification: "pass", frictionRetriesLeft: 3 },
65
- { file: "c.ts", timestamp: "", verification: "pass", frictionRetriesLeft: 3 },
66
- ]);
67
- const score = computeCoherenceScore(trace);
68
- // Protocol coverage: 5/5 * 30 = 30
69
- // Integrity: 3/3 * 35 = 35
70
- // Edge: 0/3 * 20 = 0 (no analyze phases)
71
- // Timing: 5/5 * 15 = 15
72
- // Total: 80
73
- expect(score).toBe(80);
74
- });
75
- it("penalizes verification failures", () => {
76
- const trace = makeTrace(["ambiguity_check", "four_invariants", "verification_gate", "commit_decision", "summary"], [
77
- { file: "a.ts", timestamp: "", verification: "pass", frictionRetriesLeft: 3 },
78
- { file: "b.ts", timestamp: "", verification: "fail", frictionRetriesLeft: 2 },
79
- ]);
80
- const score = computeCoherenceScore(trace);
81
- // Protocol: 5/5 * 30 = 30
82
- // Integrity: 1/2 * 35 = 17.5
83
- // Edge: 0
84
- // Timing: 5/5 * 15 = 15
85
- // Total: 62.5 -> 63
86
- expect(score).toBe(63);
87
- });
88
- it("penalizes missing protocol steps", () => {
89
- const trace = makeTrace(["ambiguity_check", "verification_gate"], [
90
- { file: "a.ts", timestamp: "", verification: "pass", frictionRetriesLeft: 3 },
91
- ]);
92
- const score = computeCoherenceScore(trace);
93
- // Protocol: 2/5 * 30 = 12
94
- // Integrity: 1/1 * 35 = 35
95
- // Edge: 0
96
- // Timing: inOrder starts with ambiguity (idx 0), then verification_gate (idx 2) -> 2
97
- // 2/5 * 15 = 6
98
- // Total: 53
99
- expect(score).toBe(53);
100
- });
101
- it("handles partial trace without crashing", () => {
102
- const trace = makeTrace([], []);
103
- // Should not crash
104
- expect(() => computeCoherenceScore(trace)).not.toThrow();
105
- });
106
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,109 +0,0 @@
1
- /**
2
- * Tests for trace recording and export.
3
- */
4
- import { describe, it, expect, beforeEach } from "vitest";
5
- function createTrace() {
6
- return {
7
- schemaVersion: "1.0",
8
- session: {
9
- id: "test-session",
10
- agent: "parallax",
11
- agentVersion: "0.2.0",
12
- startedAt: new Date().toISOString(),
13
- endedAt: null,
14
- },
15
- phases: [],
16
- writes: [],
17
- coherenceScore: null,
18
- };
19
- }
20
- function addPhase(trace, phase, data = {}) {
21
- trace.phases.push({ phase, timestamp: new Date().toISOString(), data });
22
- }
23
- function addWrite(trace, file, verdict, retries) {
24
- trace.writes.push({
25
- file,
26
- timestamp: new Date().toISOString(),
27
- verification: verdict,
28
- frictionRetriesLeft: retries,
29
- });
30
- }
31
- function computeMetrics(trace) {
32
- const totalWrites = trace.writes.length;
33
- const passes = trace.writes.filter((w) => w.verification === "pass").length;
34
- const firstPass = trace.writes.filter((w) => w.verification === "pass" && w.frictionRetriesLeft === 3).length;
35
- const phaseNames = new Set(trace.phases.map((p) => p.phase));
36
- const required = [
37
- "ambiguity_check",
38
- "four_invariants",
39
- "verification_gate",
40
- "commit_decision",
41
- "summary",
42
- ];
43
- const completed = required.filter((r) => phaseNames.has(r)).length;
44
- return {
45
- totalWrites,
46
- verificationPassRate: totalWrites > 0 ? passes / totalWrites : 0,
47
- firstAttemptPassRate: totalWrites > 0 ? firstPass / totalWrites : 0,
48
- protocolStepsCompleted: completed,
49
- };
50
- }
51
- describe("Trace recording", () => {
52
- let trace;
53
- beforeEach(() => {
54
- trace = createTrace();
55
- });
56
- it("creates an empty trace with correct schema", () => {
57
- expect(trace.schemaVersion).toBe("1.0");
58
- expect(trace.session.id).toBe("test-session");
59
- expect(trace.phases).toHaveLength(0);
60
- expect(trace.writes).toHaveLength(0);
61
- });
62
- it("records phases in order", () => {
63
- addPhase(trace, "ambiguity_check", { level: "LOW" });
64
- addPhase(trace, "four_invariants");
65
- addPhase(trace, "verification_gate");
66
- expect(trace.phases).toHaveLength(3);
67
- expect(trace.phases[0].phase).toBe("ambiguity_check");
68
- expect(trace.phases[1].phase).toBe("four_invariants");
69
- expect(trace.phases[2].phase).toBe("verification_gate");
70
- expect(trace.phases[0].data.level).toBe("LOW");
71
- });
72
- it("records writes with verification status", () => {
73
- addWrite(trace, "src/main.ts", "pass", 3);
74
- addWrite(trace, "src/lib.ts", "fail", 2);
75
- addWrite(trace, "src/utils.ts", "pass", 3);
76
- expect(trace.writes).toHaveLength(3);
77
- expect(trace.writes[0].file).toBe("src/main.ts");
78
- expect(trace.writes[1].verification).toBe("fail");
79
- expect(trace.writes[2].frictionRetriesLeft).toBe(3);
80
- });
81
- it("computes metrics from trace data", () => {
82
- addPhase(trace, "ambiguity_check");
83
- addPhase(trace, "four_invariants");
84
- addPhase(trace, "verification_gate");
85
- addPhase(trace, "commit_decision");
86
- addPhase(trace, "summary");
87
- addWrite(trace, "a.ts", "pass", 3);
88
- addWrite(trace, "b.ts", "pass", 3);
89
- addWrite(trace, "c.ts", "fail", 1);
90
- addWrite(trace, "d.ts", "pass", 3);
91
- const metrics = computeMetrics(trace);
92
- expect(metrics.totalWrites).toBe(4);
93
- expect(metrics.verificationPassRate).toBe(0.75);
94
- expect(metrics.firstAttemptPassRate).toBe(0.75); // 3 out of 4 passed on first attempt
95
- expect(metrics.protocolStepsCompleted).toBe(5);
96
- });
97
- it("handles empty trace metrics", () => {
98
- const metrics = computeMetrics(trace);
99
- expect(metrics.totalWrites).toBe(0);
100
- expect(metrics.verificationPassRate).toBe(0);
101
- expect(metrics.protocolStepsCompleted).toBe(0);
102
- });
103
- it("isolates traces by session", () => {
104
- const trace2 = createTrace();
105
- addPhase(trace, "ambiguity_check");
106
- expect(trace.phases).toHaveLength(1);
107
- expect(trace2.phases).toHaveLength(0);
108
- });
109
- });