@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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/synapse +0 -0
- package/cmd/rubriceval/main.go +308 -0
- package/cmd/synapse/main.go +280 -0
- package/cmd/synapse/main_test.go +16 -0
- package/go.mod +7 -0
- package/go.sum +6 -0
- package/internal/adapter/adapter.go +1117 -0
- package/internal/adapter/adapter_test.go +518 -0
- package/internal/adapter/agy/adapter.go +193 -0
- package/internal/adapter/claudecode/adapter.go +415 -0
- package/internal/adapter/claudecode/adapter_test.go +260 -0
- package/internal/adapter/claudecode/agents.go +387 -0
- package/internal/adapter/claudecode/agents_test.go +480 -0
- package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
- package/internal/adapter/codex/adapter.go +919 -0
- package/internal/adapter/codex/adapter_test.go +920 -0
- package/internal/adapter/codex/agents.go +401 -0
- package/internal/adapter/codex/agents_test.go +610 -0
- package/internal/adapter/codex/summary_inputs_test.go +20 -0
- package/internal/adapter/pi/adapter.go +483 -0
- package/internal/adapter/pi/adapter_test.go +517 -0
- package/internal/citymap/builder.go +1124 -0
- package/internal/citymap/builder_test.go +818 -0
- package/internal/judge/cache.go +180 -0
- package/internal/judge/cli.go +240 -0
- package/internal/judge/cli_test.go +68 -0
- package/internal/judge/fresh_summary_test.go +37 -0
- package/internal/judge/input.go +233 -0
- package/internal/judge/judge.go +416 -0
- package/internal/judge/judge_test.go +288 -0
- package/internal/judge/prompt.go +129 -0
- package/internal/judge/rubric.go +275 -0
- package/internal/judge/rubric_test.go +642 -0
- package/internal/model/agent.go +63 -0
- package/internal/model/agent_schema_test.go +86 -0
- package/internal/model/agent_test.go +64 -0
- package/internal/model/model.go +175 -0
- package/internal/model/report.go +166 -0
- package/internal/model/stats.go +151 -0
- package/internal/model/stats_test.go +89 -0
- package/internal/model/trace_schema_test.go +67 -0
- package/internal/server/analyze.go +286 -0
- package/internal/server/analyze_test.go +297 -0
- package/internal/server/codex_index_test.go +40 -0
- package/internal/server/hardening_test.go +147 -0
- package/internal/server/reportindex.go +91 -0
- package/internal/server/reportindex_test.go +116 -0
- package/internal/server/server.go +1099 -0
- package/internal/server/server_test.go +1389 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +3 -0
- package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
- package/internal/server/static/index.html +26 -0
- package/internal/server/tracestore.go +173 -0
- package/internal/server/tracestore_test.go +51 -0
- package/internal/textutil/truncate.go +30 -0
- package/internal/textutil/truncate_test.go +39 -0
- package/package.json +35 -0
- package/web/e2e/agent-lens.spec.ts +688 -0
- package/web/index.html +23 -0
- package/web/package-lock.json +1933 -0
- package/web/package.json +33 -0
- package/web/playwright.config.ts +24 -0
- package/web/src/App.tsx +876 -0
- package/web/src/api/client.ts +74 -0
- package/web/src/main.tsx +12 -0
- package/web/src/playback/recorder.ts +160 -0
- package/web/src/playback/reducer.ts +91 -0
- package/web/src/scene/CityScene.tsx +638 -0
- package/web/src/scene/TreeScene.tsx +656 -0
- package/web/src/scene/dirLabels.ts +145 -0
- package/web/src/scene/sceneUtils.ts +144 -0
- package/web/src/scene/textures.ts +60 -0
- package/web/src/scene/trail.ts +79 -0
- package/web/src/scene/treeLayout.ts +169 -0
- package/web/src/state/filters.ts +40 -0
- package/web/src/state/store.ts +83 -0
- package/web/src/styles.css +2565 -0
- package/web/src/types.ts +315 -0
- package/web/src/ui/AgentsPanel.tsx +376 -0
- package/web/src/ui/Dock.tsx +104 -0
- package/web/src/ui/Hud.tsx +335 -0
- package/web/src/ui/Inspector.tsx +107 -0
- package/web/src/ui/LogoMark.tsx +38 -0
- package/web/src/ui/ReportPanel.tsx +491 -0
- package/web/src/ui/SessionRail.tsx +316 -0
- package/web/src/ui/Timeline.tsx +458 -0
- package/web/src/ui/ViewPanel.tsx +45 -0
- package/web/src/ui/shortcuts.ts +4 -0
- package/web/tsconfig.json +21 -0
- 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
|
+
}
|