@hybridlabor-api/bdb-synapse 1.0.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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. package/web/vite.config.ts +28 -0
@@ -0,0 +1,74 @@
1
+ import type { AgentGraph, CityMap, JudgeChoice, ReportStatus, SessionMeta, Trace } from "../types";
2
+
3
+ async function getJSON<T>(url: string): Promise<T> {
4
+ const res = await fetch(url);
5
+ if (!res.ok) {
6
+ const detail = (await res.text()).trim();
7
+ throw new Error(detail || `${res.status} ${res.statusText}`);
8
+ }
9
+ return res.json() as Promise<T>;
10
+ }
11
+
12
+ // raw fetch failures read like stack noise in the toast; translate the two
13
+ // failure shapes (server gone vs. server said no) into something actionable
14
+ export function describeError(err: unknown, doing: string): string {
15
+ if (err instanceof TypeError) {
16
+ return `Can't reach the Synapse server while ${doing} — is it still running?`;
17
+ }
18
+ const detail = (err instanceof Error ? err.message : String(err)).trim();
19
+ return detail ? `Couldn't finish ${doing}: ${detail}` : `Couldn't finish ${doing}`;
20
+ }
21
+
22
+ export function listSessions(fresh = false): Promise<SessionMeta[]> {
23
+ return getJSON<SessionMeta[]>(fresh ? "/api/sessions?fresh=1" : "/api/sessions");
24
+ }
25
+
26
+ export function getTrace(key: string): Promise<Trace> {
27
+ return getJSON<Trace>(`/api/sessions/${encodeURIComponent(key)}/trace`);
28
+ }
29
+
30
+ export function getCityMap(key: string): Promise<CityMap> {
31
+ return getJSON<CityMap>(`/api/sessions/${encodeURIComponent(key)}/citymap`);
32
+ }
33
+
34
+ export function getSessionSnapshot(key: string): Promise<{ trace: Trace; city: CityMap }> {
35
+ return getJSON<{ trace: Trace; city: CityMap }>(`/api/sessions/${encodeURIComponent(key)}/snapshot`);
36
+ }
37
+
38
+ export function getSessionAgents(rootKey: string): Promise<AgentGraph> {
39
+ return getJSON<AgentGraph>(`/api/sessions/${encodeURIComponent(rootKey)}/agents`);
40
+ }
41
+
42
+ export function getAgentTrace(rootKey: string, agentID: string): Promise<Trace> {
43
+ return getJSON<Trace>(
44
+ `/api/sessions/${encodeURIComponent(rootKey)}/agents/${encodeURIComponent(agentID)}/trace`
45
+ );
46
+ }
47
+
48
+ export function getSessionReport(key: string): Promise<ReportStatus> {
49
+ return getJSON<ReportStatus>(`/api/sessions/${encodeURIComponent(key)}/report`);
50
+ }
51
+
52
+ // kicks off a judge run on the server; progress is observed by polling
53
+ // getSessionReport until state leaves "running". The choice picks judge CLI
54
+ // and model; omitted fields fall back to the server's defaults.
55
+ export async function startSessionAnalyze(key: string, choice?: JudgeChoice): Promise<ReportStatus> {
56
+ const res = await fetch(`/api/sessions/${encodeURIComponent(key)}/analyze`, {
57
+ method: "POST",
58
+ headers: choice ? { "Content-Type": "application/json" } : undefined,
59
+ body: choice ? JSON.stringify(choice) : undefined
60
+ });
61
+ if (!res.ok) {
62
+ const detail = (await res.text()).trim();
63
+ throw new Error(detail || `${res.status} ${res.statusText}`);
64
+ }
65
+ return res.json() as Promise<ReportStatus>;
66
+ }
67
+
68
+ // backs the static full-repo map view: the citymap for a repo, with no session
69
+ // or trace attached. Without a repo path the server falls back to its
70
+ // configured RepoRoot (the `mindwalk map <repo>` case).
71
+ export function getRepoMap(repo?: string): Promise<CityMap> {
72
+ const url = repo ? `/api/repomap?repo=${encodeURIComponent(repo)}` : "/api/repomap";
73
+ return getJSON<CityMap>(url);
74
+ }
@@ -0,0 +1,12 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "@fontsource-variable/fraunces/standard.css";
4
+ import "@fontsource-variable/fraunces/standard-italic.css";
5
+ import "@fontsource-variable/schibsted-grotesk";
6
+ import App from "./App";
7
+
8
+ createRoot(document.getElementById("root")!).render(
9
+ <StrictMode>
10
+ <App />
11
+ </StrictMode>
12
+ );
@@ -0,0 +1,160 @@
1
+ // Client-side video export: capture the live WebGL canvas with MediaRecorder
2
+ // while driving the playhead from the first event to the last, then hand back a
3
+ // webm Blob. No server, no extra dependency — captureStream + MediaRecorder are
4
+ // native browser APIs.
5
+
6
+ export interface RecordOptions {
7
+ canvas: HTMLCanvasElement;
8
+ total: number; // number of playback events (trace.events.length)
9
+ setSeq: (seq: number) => void; // drives the store's currentSeq
10
+ fps?: number; // capture frame rate
11
+ durationMs?: number; // wall-clock length of the exported run
12
+ onProgress?: (fraction: number) => void; // 0..1
13
+ signal?: AbortSignal; // cancel an in-flight recording
14
+ }
15
+
16
+ export interface RecordResult {
17
+ blob: Blob;
18
+ extension: string; // matches the recorded container (webm / mp4 / …)
19
+ }
20
+
21
+ // Map a MediaRecorder mimeType to a sensible file extension. Falls back to the
22
+ // container subtype when it's an unknown-but-plausible type.
23
+ function extensionForMime(mimeType: string): string {
24
+ const base = mimeType.split(";")[0].trim().toLowerCase(); // strip codecs
25
+ const subtype = base.split("/")[1] ?? "";
26
+ if (subtype.includes("webm")) return "webm";
27
+ if (subtype.includes("mp4")) return "mp4";
28
+ if (subtype.includes("x-matroska") || subtype.includes("matroska")) return "mkv";
29
+ if (subtype.includes("ogg")) return "ogv";
30
+ return subtype || "webm";
31
+ }
32
+
33
+ const DEFAULT_FPS = 30;
34
+ // Long sessions would otherwise produce minutes of video; cap the run and let
35
+ // the playhead cover more events per frame. Short sessions play close to
36
+ // real-ish time via the per-event floor.
37
+ const MAX_DURATION_MS = 30_000;
38
+ const PER_EVENT_MS = 120;
39
+
40
+ function pickMimeType(): string | undefined {
41
+ const candidates = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"];
42
+ for (const type of candidates) {
43
+ if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(type)) return type;
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ export function recordingSupported(): boolean {
49
+ return (
50
+ typeof MediaRecorder !== "undefined" &&
51
+ typeof HTMLCanvasElement !== "undefined" &&
52
+ typeof HTMLCanvasElement.prototype.captureStream === "function"
53
+ );
54
+ }
55
+
56
+ // Records the playback and resolves with the recorded blob and a file extension
57
+ // that matches the actual container. Rejects if the browser can't record or the
58
+ // run is aborted.
59
+ export function recordPlayback(opts: RecordOptions): Promise<RecordResult> {
60
+ const { canvas, total, setSeq, onProgress, signal } = opts;
61
+ const fps = opts.fps ?? DEFAULT_FPS;
62
+
63
+ if (!recordingSupported()) {
64
+ return Promise.reject(new Error("This browser can't record the canvas (MediaRecorder unavailable)."));
65
+ }
66
+ if (total <= 0) {
67
+ return Promise.reject(new Error("Nothing to record — load a session first."));
68
+ }
69
+
70
+ const durationMs = opts.durationMs ?? Math.min(MAX_DURATION_MS, Math.max(1000, total * PER_EVENT_MS));
71
+ const mimeType = pickMimeType();
72
+ const stream = canvas.captureStream(fps);
73
+ const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
74
+ const chunks: BlobPart[] = [];
75
+
76
+ return new Promise<RecordResult>((resolve, reject) => {
77
+ let rafId = 0;
78
+ let startTs = 0;
79
+ let settled = false;
80
+
81
+ const cleanup = () => {
82
+ if (rafId) cancelAnimationFrame(rafId);
83
+ stream.getTracks().forEach((track) => track.stop());
84
+ signal?.removeEventListener("abort", onAbort);
85
+ };
86
+
87
+ const finish = () => {
88
+ if (settled) return;
89
+ settled = true;
90
+ cleanup();
91
+ // the browser may have chosen a container different from what we
92
+ // requested; trust recorder.mimeType so the blob type and file extension
93
+ // match what was actually recorded
94
+ const actualType = recorder.mimeType || mimeType || "video/webm";
95
+ resolve({ blob: new Blob(chunks, { type: actualType }), extension: extensionForMime(actualType) });
96
+ };
97
+
98
+ const fail = (err: Error) => {
99
+ if (settled) return;
100
+ settled = true;
101
+ cleanup();
102
+ try {
103
+ if (recorder.state !== "inactive") recorder.stop();
104
+ } catch {
105
+ // recorder already torn down
106
+ }
107
+ reject(err);
108
+ };
109
+
110
+ const onAbort = () => fail(new Error("Recording cancelled."));
111
+
112
+ recorder.ondataavailable = (e) => {
113
+ if (e.data && e.data.size > 0) chunks.push(e.data);
114
+ };
115
+ recorder.onerror = () => fail(new Error("Recording failed."));
116
+ recorder.onstop = finish;
117
+
118
+ if (signal) {
119
+ if (signal.aborted) {
120
+ fail(new Error("Recording cancelled."));
121
+ return;
122
+ }
123
+ signal.addEventListener("abort", onAbort);
124
+ }
125
+
126
+ const tick = (ts: number) => {
127
+ if (settled) return;
128
+ if (!startTs) startTs = ts;
129
+ const fraction = Math.min(1, (ts - startTs) / durationMs);
130
+ const seq = Math.min(total - 1, Math.floor(fraction * (total - 1)));
131
+ setSeq(seq);
132
+ onProgress?.(fraction);
133
+ if (fraction >= 1) {
134
+ // let the final frame paint before closing the recorder
135
+ rafId = requestAnimationFrame(() => {
136
+ if (recorder.state !== "inactive") recorder.stop();
137
+ });
138
+ return;
139
+ }
140
+ rafId = requestAnimationFrame(tick);
141
+ };
142
+
143
+ setSeq(0);
144
+ recorder.start();
145
+ rafId = requestAnimationFrame(tick);
146
+ });
147
+ }
148
+
149
+ // Triggers a browser download of the recorded Blob.
150
+ export function downloadBlob(blob: Blob, filename: string): void {
151
+ const url = URL.createObjectURL(blob);
152
+ const a = document.createElement("a");
153
+ a.href = url;
154
+ a.download = filename;
155
+ document.body.appendChild(a);
156
+ a.click();
157
+ a.remove();
158
+ // revoke on the next tick so the download has a chance to start
159
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
160
+ }
@@ -0,0 +1,91 @@
1
+ import type { CityMap, Target, Touch, Trace, TraceEvent } from "../types";
2
+
3
+ export const touchRank: Record<Touch, number> = {
4
+ hit: 1,
5
+ read: 2,
6
+ edit: 3
7
+ };
8
+
9
+ export interface FilePlayback {
10
+ touchByFile: Map<number, Touch>;
11
+ touchByPath: Map<string, Touch>;
12
+ visitsByFile: Map<number, number>;
13
+ historyByPath: Map<string, TraceEvent[]>;
14
+ recentTargets: Target[];
15
+ }
16
+
17
+ const RECENT_WINDOW = 12;
18
+
19
+ // Incremental playback state. Stepping the playhead forward applies only the
20
+ // events it crossed; scrubbing backward replays from the start once. Snapshots
21
+ // share the engine's live collections — consumers must treat them as read-only
22
+ // and re-read whenever the snapshot identity changes (one fresh wrapper per
23
+ // snapshotAt call).
24
+ export class PlaybackEngine {
25
+ private readonly events: TraceEvent[];
26
+ private readonly idByPath = new Map<string, number>();
27
+ private cursor = -1;
28
+ private readonly touchByFile = new Map<number, Touch>();
29
+ private readonly touchByPath = new Map<string, Touch>();
30
+ private readonly visitsByFile = new Map<number, number>();
31
+ private readonly historyByPath = new Map<string, TraceEvent[]>();
32
+ private readonly recentTargets: Target[] = [];
33
+
34
+ constructor(trace: Trace | undefined, city: CityMap | undefined) {
35
+ this.events = trace?.events ?? [];
36
+ for (const file of city?.files ?? []) {
37
+ this.idByPath.set(file.path, file.id);
38
+ }
39
+ }
40
+
41
+ snapshotAt(seq: number): FilePlayback {
42
+ const lastSeq = Math.min(seq, this.events.length - 1);
43
+ if (lastSeq < this.cursor) this.reset();
44
+ for (let i = this.cursor + 1; i <= lastSeq; i++) {
45
+ this.apply(this.events[i]);
46
+ }
47
+ this.cursor = Math.max(this.cursor, lastSeq);
48
+ return {
49
+ touchByFile: this.touchByFile,
50
+ touchByPath: this.touchByPath,
51
+ visitsByFile: this.visitsByFile,
52
+ historyByPath: this.historyByPath,
53
+ recentTargets: this.recentTargets
54
+ };
55
+ }
56
+
57
+ private reset() {
58
+ this.cursor = -1;
59
+ this.touchByFile.clear();
60
+ this.touchByPath.clear();
61
+ this.visitsByFile.clear();
62
+ this.historyByPath.clear();
63
+ this.recentTargets.length = 0;
64
+ }
65
+
66
+ private apply(event: TraceEvent) {
67
+ for (const target of event.targets) {
68
+ const prev = this.touchByPath.get(target.path);
69
+ if (!prev || touchRank[target.touch] > touchRank[prev]) {
70
+ this.touchByPath.set(target.path, target.touch);
71
+ }
72
+ const fileId = target.fileId ?? this.idByPath.get(target.path);
73
+ if (fileId !== undefined) {
74
+ const prevFile = this.touchByFile.get(fileId);
75
+ if (!prevFile || touchRank[target.touch] > touchRank[prevFile]) {
76
+ this.touchByFile.set(fileId, target.touch);
77
+ }
78
+ this.visitsByFile.set(fileId, (this.visitsByFile.get(fileId) ?? 0) + 1);
79
+ }
80
+ const history = this.historyByPath.get(target.path);
81
+ if (history) history.push(event);
82
+ else this.historyByPath.set(target.path, [event]);
83
+ }
84
+ if (event.targets.length > 0) {
85
+ // the trail follows the strongest signal: skip weak (heuristic) targets
86
+ const primary = event.targets.find((target) => !target.weak) ?? event.targets[0];
87
+ this.recentTargets.push({ ...primary, fileId: primary.fileId ?? this.idByPath.get(primary.path) });
88
+ if (this.recentTargets.length > RECENT_WINDOW) this.recentTargets.shift();
89
+ }
90
+ }
91
+ }