@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,26 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="color-scheme" content="dark" />
7
+ <meta name="theme-color" content="#12151c" />
8
+ <link
9
+ rel="icon"
10
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3CradialGradient id='halo'%3E%3Cstop offset='0' stop-color='%23f1aa57' stop-opacity='0.55'/%3E%3Cstop offset='1' stop-color='%23f1aa57' stop-opacity='0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='%230f141b'/%3E%3Ccircle cx='20' cy='15' r='1.6' fill='%233b4047'/%3E%3Ccircle cx='11' cy='27' r='1.4' fill='%233b4047'/%3E%3Ccircle cx='51' cy='40' r='1.6' fill='%233b4047'/%3E%3Ccircle cx='34' cy='53' r='1.4' fill='%233b4047'/%3E%3Cg fill='%23f1aa57'%3E%3Ccircle cx='18.6' cy='47.9' r='1.15' opacity='0.35'/%3E%3Ccircle cx='22.7' cy='45.2' r='1.25' opacity='0.42'/%3E%3Ccircle cx='25.9' cy='42.0' r='1.35' opacity='0.49'/%3E%3Ccircle cx='28.2' cy='38.1' r='1.45' opacity='0.56'/%3E%3Ccircle cx='30.9' cy='29.3' r='1.55' opacity='0.64'/%3E%3Ccircle cx='33.0' cy='25.4' r='1.65' opacity='0.72'/%3E%3Ccircle cx='36.0' cy='22.1' r='1.75' opacity='0.8'/%3E%3Ccircle cx='38.0' cy='20.4' r='1.85' opacity='0.88'/%3E%3C/g%3E%3Ccircle cx='13' cy='50' r='4.5' fill='%239fc67b' opacity='0.16'/%3E%3Ccircle cx='13' cy='50' r='2.4' fill='%239fc67b'/%3E%3Ccircle cx='29.5' cy='34' r='5' fill='%23b6daf1' opacity='0.16'/%3E%3Ccircle cx='29.5' cy='34' r='2.7' fill='%23b6daf1'/%3E%3Ccircle cx='44' cy='17.5' r='9.5' fill='url(%23halo)'/%3E%3Ccircle cx='44' cy='17.5' r='3.4' fill='%23f1aa57'/%3E%3Ccircle cx='44' cy='17.5' r='1.7' fill='%23ffe0b0'/%3E%3C/svg%3E"
11
+ />
12
+ <title>mindwalk</title>
13
+ <style>
14
+ html {
15
+ background: #12151c;
16
+ }
17
+ </style>
18
+ <script type="module" crossorigin src="/assets/index-C_adLrJr.js"></script>
19
+ <link rel="modulepreload" crossorigin href="/assets/react-gcHzaSmV.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/three-DnGjZfD1.js">
21
+ <link rel="stylesheet" crossorigin href="/assets/index-BNoY_BiB.css">
22
+ </head>
23
+ <body>
24
+ <div id="root"></div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,173 @@
1
+ package server
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "runtime/debug"
7
+ "sync"
8
+ "time"
9
+
10
+ "github.com/hybridlabor-api/bdb-synapse/internal/adapter"
11
+ "github.com/hybridlabor-api/bdb-synapse/internal/model"
12
+ )
13
+
14
+ // traceStore owns every parsed-session cache behind one lock. It keeps two
15
+ // layers with the same key space (session key) but different value semantics:
16
+ //
17
+ // - snapshots: the trace projected onto its own citymap (file IDs assigned,
18
+ // stats recomputed against the repo file count) plus that citymap — what
19
+ // the session views serve.
20
+ // - raws: the trace exactly as the adapter parsed it, no projection — what
21
+ // subagent views load before projecting onto the *root* session's city.
22
+ //
23
+ // Keeping the layers apart means a projected snapshot can never be
24
+ // re-projected and a raw trace can never be served where file IDs are
25
+ // expected. Keys are session keys; the source-file fingerprint is a
26
+ // validation field on the entry — never part of the key — so one session
27
+ // occupies at most one slot per layer.
28
+ type traceStore struct {
29
+ mu sync.Mutex
30
+ snapshots map[string]*traceEntry
31
+ raws map[string]*traceEntry
32
+ inflight map[string]*inflightLoad
33
+
34
+ // Injected by the server: parse-and-project for snapshots, parse-only for
35
+ // raws. Called without the store lock held.
36
+ loadSnapshot func(model.SessionMeta) (*model.Trace, *model.CityMap, error)
37
+ loadRaw func(model.SessionMeta) (*model.Trace, error)
38
+ }
39
+
40
+ type traceEntry struct {
41
+ trace *model.Trace
42
+ city *model.CityMap // nil in the raw layer
43
+ fingerprint fileFingerprint
44
+ at time.Time
45
+ used time.Time
46
+ }
47
+
48
+ type inflightLoad struct {
49
+ done chan struct{}
50
+ fingerprint fileFingerprint
51
+ trace *model.Trace
52
+ city *model.CityMap
53
+ err error
54
+ }
55
+
56
+ func newTraceStore(loadSnapshot func(model.SessionMeta) (*model.Trace, *model.CityMap, error), loadRaw func(model.SessionMeta) (*model.Trace, error)) *traceStore {
57
+ return &traceStore{
58
+ snapshots: map[string]*traceEntry{},
59
+ raws: map[string]*traceEntry{},
60
+ inflight: map[string]*inflightLoad{},
61
+ loadSnapshot: loadSnapshot,
62
+ loadRaw: loadRaw,
63
+ }
64
+ }
65
+
66
+ func (ts *traceStore) LoadSnapshot(meta model.SessionMeta) (*model.Trace, *model.CityMap, error) {
67
+ return ts.load(ts.snapshots, "snapshot", meta, ts.loadSnapshot)
68
+ }
69
+
70
+ func (ts *traceStore) LoadRaw(meta model.SessionMeta) (*model.Trace, error) {
71
+ trace, _, err := ts.load(ts.raws, "raw", meta, func(meta model.SessionMeta) (*model.Trace, *model.CityMap, error) {
72
+ trace, err := ts.loadRaw(meta)
73
+ return trace, nil, err
74
+ })
75
+ return trace, err
76
+ }
77
+
78
+ func (ts *traceStore) load(layer map[string]*traceEntry, kind string, meta model.SessionMeta, loader func(model.SessionMeta) (*model.Trace, *model.CityMap, error)) (*model.Trace, *model.CityMap, error) {
79
+ key := meta.Key
80
+ if key == "" {
81
+ key = adapter.SessionKey(meta.Harness, meta.Path)
82
+ }
83
+ inflightKey := kind + "\x00" + key
84
+ for {
85
+ fingerprint, err := fingerprintFile(meta.Path)
86
+ if err != nil {
87
+ ts.mu.Lock()
88
+ delete(layer, key)
89
+ ts.mu.Unlock()
90
+ return nil, nil, err
91
+ }
92
+
93
+ ts.mu.Lock()
94
+ if entry := layer[key]; entry != nil {
95
+ if entry.fingerprint.equal(fingerprint) && time.Since(entry.at) < traceCacheTTL {
96
+ entry.used = time.Now()
97
+ trace, city := entry.trace, entry.city
98
+ ts.mu.Unlock()
99
+ return trace, city, nil
100
+ }
101
+ delete(layer, key)
102
+ }
103
+ if load := ts.inflight[inflightKey]; load != nil {
104
+ done := load.done
105
+ shareSnapshot := fingerprint.equal(load.fingerprint)
106
+ ts.mu.Unlock()
107
+ <-done
108
+
109
+ // Requests that observed the same source version must receive the
110
+ // same trace/city snapshot, even if the active file grows while the
111
+ // shared parse is running. A request that already observed a newer
112
+ // version retries after the older load completes.
113
+ if shareSnapshot {
114
+ return load.trace, load.city, load.err
115
+ }
116
+ continue
117
+ }
118
+ load := &inflightLoad{done: make(chan struct{}), fingerprint: fingerprint}
119
+ ts.inflight[inflightKey] = load
120
+ ts.mu.Unlock()
121
+
122
+ // Keep the pre-parse fingerprint. If the active session grows during
123
+ // parsing, the next request will see a mismatch and reload it instead
124
+ // of treating the partial snapshot as current.
125
+ ts.run(layer, inflightKey, key, load, meta, loader)
126
+ return load.trace, load.city, load.err
127
+ }
128
+ }
129
+
130
+ // run executes the shared load for key and publishes the result on load. The
131
+ // finalize step — cache the result, drop the inflight entry, close load.done
132
+ // — runs in a defer so a panicking loader cannot skip it. Without that,
133
+ // net/http's per-connection recover would swallow the panic while the
134
+ // inflight entry stayed registered, and every later request for the key
135
+ // would block forever on a done channel nothing closes.
136
+ func (ts *traceStore) run(layer map[string]*traceEntry, inflightKey, key string, load *inflightLoad, meta model.SessionMeta, loader func(model.SessionMeta) (*model.Trace, *model.CityMap, error)) {
137
+ defer func() {
138
+ if r := recover(); r != nil {
139
+ load.trace, load.city = nil, nil
140
+ load.err = fmt.Errorf("load session %s: %v", key, r)
141
+ log.Printf("mindwalk: panic loading session %s: %v\n%s", key, r, debug.Stack())
142
+ }
143
+ ts.mu.Lock()
144
+ if load.err == nil {
145
+ now := time.Now()
146
+ layer[key] = &traceEntry{trace: load.trace, city: load.city, fingerprint: load.fingerprint, at: now, used: now}
147
+ ts.evictLocked(layer)
148
+ }
149
+ delete(ts.inflight, inflightKey)
150
+ close(load.done)
151
+ ts.mu.Unlock()
152
+ }()
153
+ load.trace, load.city, load.err = loader(meta)
154
+ }
155
+
156
+ // evictLocked bounds one layer by dropping the least recently used entries
157
+ // once it grows past traceCacheMaxEntries. Caller must hold mu.
158
+ func (ts *traceStore) evictLocked(layer map[string]*traceEntry) {
159
+ for len(layer) > traceCacheMaxEntries {
160
+ var oldestKey string
161
+ var oldest time.Time
162
+ for key, entry := range layer {
163
+ if oldestKey == "" || entry.used.Before(oldest) {
164
+ oldestKey = key
165
+ oldest = entry.used
166
+ }
167
+ }
168
+ if oldestKey == "" {
169
+ return
170
+ }
171
+ delete(layer, oldestKey)
172
+ }
173
+ }
@@ -0,0 +1,51 @@
1
+ package server
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+
8
+ "github.com/hybridlabor-api/bdb-synapse/internal/model"
9
+ )
10
+
11
+ // The raw and snapshot layers share a key space but must never share entries:
12
+ // a session loaded through both APIs is parsed once per layer and each layer
13
+ // serves its own cached value afterwards.
14
+ func TestTraceStoreLayersAreIndependent(t *testing.T) {
15
+ session := filepath.Join(t.TempDir(), "s.jsonl")
16
+ if err := os.WriteFile(session, []byte("{}\n"), 0o644); err != nil {
17
+ t.Fatal(err)
18
+ }
19
+ meta := model.SessionMeta{Key: "s", Harness: "test", Path: session}
20
+
21
+ snapshotLoads, rawLoads := 0, 0
22
+ snapshotTrace := &model.Trace{Session: model.TraceSession{ID: "projected"}}
23
+ rawTrace := &model.Trace{Session: model.TraceSession{ID: "raw"}}
24
+ city := &model.CityMap{}
25
+ ts := newTraceStore(
26
+ func(model.SessionMeta) (*model.Trace, *model.CityMap, error) {
27
+ snapshotLoads++
28
+ return snapshotTrace, city, nil
29
+ },
30
+ func(model.SessionMeta) (*model.Trace, error) {
31
+ rawLoads++
32
+ return rawTrace, nil
33
+ },
34
+ )
35
+
36
+ for i := 0; i < 2; i++ {
37
+ trace, gotCity, err := ts.LoadSnapshot(meta)
38
+ if err != nil || trace != snapshotTrace || gotCity != city {
39
+ t.Fatalf("snapshot load %d: trace=%v city=%v err=%v", i, trace, gotCity, err)
40
+ }
41
+ }
42
+ for i := 0; i < 2; i++ {
43
+ trace, err := ts.LoadRaw(meta)
44
+ if err != nil || trace != rawTrace {
45
+ t.Fatalf("raw load %d: trace=%v err=%v", i, trace, err)
46
+ }
47
+ }
48
+ if snapshotLoads != 1 || rawLoads != 1 {
49
+ t.Fatalf("loads = snapshot:%d raw:%d, want 1 each", snapshotLoads, rawLoads)
50
+ }
51
+ }
@@ -0,0 +1,30 @@
1
+ // Package textutil contains UTF-8-safe helpers shared across trace ingestion
2
+ // and judge rendering.
3
+ package textutil
4
+
5
+ import "unicode/utf8"
6
+
7
+ // TruncateRunes limits text to at most limit runes, including marker. Invalid
8
+ // UTF-8 is replaced with the standard replacement rune so callers can safely
9
+ // serialize the result. When the marker itself exceeds the budget, its prefix
10
+ // takes the available space.
11
+ func TruncateRunes(text string, limit int, marker string) string {
12
+ if limit <= 0 {
13
+ return ""
14
+ }
15
+
16
+ runes := []rune(text)
17
+ if len(runes) <= limit {
18
+ if utf8.ValidString(text) {
19
+ return text
20
+ }
21
+ return string(runes)
22
+ }
23
+
24
+ markerRunes := []rune(marker)
25
+ if len(markerRunes) > limit {
26
+ markerRunes = markerRunes[:limit]
27
+ }
28
+ contentLimit := limit - len(markerRunes)
29
+ return string(runes[:contentLimit]) + string(markerRunes)
30
+ }
@@ -0,0 +1,39 @@
1
+ package textutil
2
+
3
+ import (
4
+ "testing"
5
+ "unicode/utf8"
6
+ )
7
+
8
+ func TestTruncateRunes(t *testing.T) {
9
+ tests := []struct {
10
+ name string
11
+ text string
12
+ limit int
13
+ marker string
14
+ want string
15
+ }{
16
+ {name: "unchanged", text: "你好", limit: 2, marker: "...", want: "你好"},
17
+ {name: "multibyte boundary", text: "aa界tail", limit: 6, marker: "...", want: "aa界..."},
18
+ {name: "emoji boundary", text: "aa🙂tail", limit: 6, marker: "...", want: "aa🙂..."},
19
+ {name: "marker within budget", text: "abcdef", limit: 4, marker: "…", want: "abc…"},
20
+ {name: "marker fills budget", text: "abcdef", limit: 2, marker: "...", want: ".."},
21
+ {name: "zero budget", text: "abcdef", limit: 0, marker: "…", want: ""},
22
+ {name: "invalid input", text: string([]byte{'o', 'k', 0xff}), limit: 4, want: "ok�"},
23
+ }
24
+
25
+ for _, tt := range tests {
26
+ t.Run(tt.name, func(t *testing.T) {
27
+ got := TruncateRunes(tt.text, tt.limit, tt.marker)
28
+ if got != tt.want {
29
+ t.Fatalf("TruncateRunes() = %q, want %q", got, tt.want)
30
+ }
31
+ if !utf8.ValidString(got) {
32
+ t.Fatalf("TruncateRunes() returned invalid UTF-8: %q", got)
33
+ }
34
+ if gotRunes := len([]rune(got)); gotRunes > tt.limit {
35
+ t.Fatalf("TruncateRunes() returned %d runes, limit %d", gotRunes, tt.limit)
36
+ }
37
+ })
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@hybridlabor-api/bdb-synapse",
3
+ "version": "1.0.0",
4
+ "description": "BDB Synapse — 3D Codebase Visualizer & Agent Session Replay Engine",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/hybridlabor-api/bdb-synapse.git"
9
+ },
10
+ "keywords": [
11
+ "bdb",
12
+ "synapse",
13
+ "3d",
14
+ "codebase",
15
+ "visualization",
16
+ "agent",
17
+ "session-replay",
18
+ "webgl",
19
+ "threejs"
20
+ ],
21
+ "files": [
22
+ "bin/",
23
+ "internal/server/static/",
24
+ "cmd/",
25
+ "internal/",
26
+ "web/",
27
+ "go.mod",
28
+ "go.sum",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }