Skip to content

Commit a300cf6

Browse files
radimsemclaude
andauthored
refactor: apply functional options to long param functions (#153)
## Summary Closes #145 Closes #146 Closes #147 ## Self-review ### Touched **Pipeline** - [ ] `pkg/parser/` — fuzz target covers any new shape - [ ] `pkg/transformer/` or `pkg/diff/` — ID stability / hash inputs unchanged - [ ] `pkg/emitter/` — exactly one snapshot per write - [x] `pkg/store/` + `migrations/` — FTS5 triggers in sync; `add-store-query` followed - [ ] `pkg/query/` or `pkg/compiler/` — token budget honored **MCP surface** - [x] `pkg/mcp/` — `skills/remind` (read) or `skills/memoize` (write) updated - [ ] `pkg/temperature/` — both public skills reflect new values **Edges** - [ ] `cmd/remindb/` — README CLI section updated - [ ] `internal/ignore/`, `internal/tempfile/`, `internal/bench/` — relevant fuzz / fixture / scenario added - [ ] `plugins/<agent>/` — manifest version bumped if shipping ### Process - [ ] Tested manually via CLI or local MCP plugin install - [x] `make test-all` + `make fuzz` green --------- Signed-off-by: radimsem <me@radimsemerak.cz> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f66288 commit a300cf6

20 files changed

Lines changed: 309 additions & 167 deletions

File tree

cmd/remindb/serve.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/radimsem/remindb/pkg/config"
1414
"github.com/radimsem/remindb/pkg/logbuf"
1515
remindb "github.com/radimsem/remindb/pkg/mcp"
16+
"github.com/radimsem/remindb/pkg/mcp/rescan"
1617
"github.com/radimsem/remindb/pkg/mcp/rescanlog"
1718
"github.com/radimsem/remindb/pkg/mcp/rescanstat"
1819
"github.com/radimsem/remindb/pkg/mcp/sessionlog"
@@ -174,14 +175,19 @@ func runServe(cmd *cobra.Command, _ []string) error {
174175
return err
175176
}
176177

177-
rescan, err := remindb.NewRescanLoop(st, sourceDir, rescanInterval, workspaceCfg.Compile, logger, rescanStatus, rescanLog)
178+
rescanLoop, err := rescan.New(st, sourceDir, rescanInterval,
179+
rescan.WithCompileConfig(workspaceCfg.Compile),
180+
rescan.WithLogger(logger),
181+
rescan.WithStatus(rescanStatus),
182+
rescan.WithRescanLog(rescanLog),
183+
)
178184
if err != nil {
179185
return err
180186
}
181-
rescan.SetChangeObserver(srv.NotifyRescan)
187+
rescanLoop.SetChangeObserver(srv.NotifyRescan)
182188

183189
g.Go(func() error {
184-
rescan.Run(ctx)
190+
rescanLoop.Run(ctx)
185191
return nil
186192
})
187193
}

internal/mcptest/mcptest.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/radimsem/remindb/pkg/config"
1616
"github.com/radimsem/remindb/pkg/logbuf"
1717
remindb "github.com/radimsem/remindb/pkg/mcp"
18+
"github.com/radimsem/remindb/pkg/mcp/rescan"
1819
"github.com/radimsem/remindb/pkg/mcp/rescanstat"
1920
"github.com/radimsem/remindb/pkg/mcp/sessionlog"
2021
"github.com/radimsem/remindb/pkg/store"
@@ -188,9 +189,9 @@ func NewEnvWithRescan(t *testing.T) *Env {
188189
t.Fatalf("NewServer: %v", err)
189190
}
190191

191-
loop, err := remindb.NewRescanLoop(st, dir, time.Second, config.CompileConfig{}, nil, status, nil)
192+
loop, err := rescan.New(st, dir, time.Second, rescan.WithStatus(status))
192193
if err != nil {
193-
t.Fatalf("NewRescanLoop: %v", err)
194+
t.Fatalf("rescan.New: %v", err)
194195
}
195196

196197
loopCtx, cancelLoop := context.WithCancel(context.Background())

pkg/compiler/compiler_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ func TestCompile_WikilinkResolvesCrossFile(t *testing.T) {
831831
t.Fatalf("no source paragraph found in a.md: %+v", aNodes)
832832
}
833833

834-
related, err := st.GetRelatedNodes(ctx, sourceID, store.DirectionOut, 1, 0, 10)
834+
related, err := st.GetRelatedNodes(ctx, sourceID, store.WithDirection(store.DirectionOut), store.WithMaxDepth(1), store.WithLimit(10))
835835
if err != nil {
836836
t.Fatalf("GetRelatedNodes: %v", err)
837837
}
@@ -910,7 +910,7 @@ func TestCompile_WikilinkPendingResolvesOnLaterCompile(t *testing.T) {
910910
t.Fatalf("no source paragraph found in a.md after second compile: %+v", aNodes)
911911
}
912912

913-
related, err := st.GetRelatedNodes(ctx, sourceID, store.DirectionOut, 1, 0, 10)
913+
related, err := st.GetRelatedNodes(ctx, sourceID, store.WithDirection(store.DirectionOut), store.WithMaxDepth(1), store.WithLimit(10))
914914
if err != nil {
915915
t.Fatalf("GetRelatedNodes: %v", err)
916916
}

pkg/emitter/emitter.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ func Emit(ctx context.Context, st *store.Store, opts ...Option) error {
8181
}
8282
}
8383

84-
snapID, err := st.CreateSnapshotTx(ctx, tx, o.cursorHash, o.message, o.compileRoot)
84+
snapID, err := st.CreateSnapshotTx(ctx, tx,
85+
store.WithCursorHash(o.cursorHash),
86+
store.WithMessage(o.message),
87+
store.WithCompileRoot(o.compileRoot),
88+
)
8589
if err != nil {
8690
return fmt.Errorf("failed to create: snapshot: %w", err)
8791
}

pkg/mcp/initial_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/radimsem/remindb/internal/testutil"
10+
)
11+
12+
func TestMaybeInitialCompile_EmptyDB(t *testing.T) {
13+
dir := t.TempDir()
14+
writeFile(t, dir, "a.md", "# A\n\nBody.\n")
15+
writeFile(t, dir, "b.md", "# B\n\nBody.\n")
16+
17+
st := testutil.OpenTestDB(t)
18+
ctx := context.Background()
19+
20+
if err := MaybeInitialCompile(ctx, st, dir, nil); err != nil {
21+
t.Fatalf("MaybeInitialCompile: %v", err)
22+
}
23+
24+
roots, err := st.GetRootNodes(ctx)
25+
if err != nil {
26+
t.Fatalf("GetRootNodes: %v", err)
27+
}
28+
if len(roots) == 0 {
29+
t.Error("expected nodes to be compiled on empty DB")
30+
}
31+
}
32+
33+
func TestMaybeInitialCompile_NonEmptyDB(t *testing.T) {
34+
dir := t.TempDir()
35+
writeFile(t, dir, "a.md", "# A\n\nBody.\n")
36+
37+
st := testutil.OpenTestDB(t)
38+
ctx := context.Background()
39+
40+
if err := MaybeInitialCompile(ctx, st, dir, nil); err != nil {
41+
t.Fatalf("seed compile: %v", err)
42+
}
43+
44+
before, err := st.GetRootNodes(ctx)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
writeFile(t, dir, "new.md", "# New\n")
50+
51+
if err := MaybeInitialCompile(ctx, st, dir, nil); err != nil {
52+
t.Fatalf("MaybeInitialCompile: %v", err)
53+
}
54+
55+
after, err := st.GetRootNodes(ctx)
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
if len(after) != len(before) {
60+
t.Errorf("root count changed: before=%d after=%d (MaybeInitialCompile ran on non-empty DB)", len(before), len(after))
61+
}
62+
}
63+
64+
func writeFile(t *testing.T, dir, name, content string) {
65+
t.Helper()
66+
p := filepath.Join(dir, name)
67+
68+
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
69+
t.Fatal(err)
70+
}
71+
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
72+
t.Fatal(err)
73+
}
74+
}
Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
package mcp
1+
// Package rescan periodically recompiles a source tree into the store.
2+
package rescan
23

34
import (
45
"context"
@@ -31,7 +32,32 @@ const (
3132
defaultSettleTime = 500 * time.Millisecond
3233
)
3334

34-
type RescanLoop struct {
35+
type Option func(*options)
36+
37+
type options struct {
38+
compileConfig config.CompileConfig
39+
logger *slog.Logger
40+
status *rescanstat.Status
41+
rescanLog *rescanlog.Sink
42+
}
43+
44+
func WithCompileConfig(cc config.CompileConfig) Option {
45+
return func(o *options) { o.compileConfig = cc }
46+
}
47+
48+
func WithLogger(l *slog.Logger) Option {
49+
return func(o *options) { o.logger = l }
50+
}
51+
52+
func WithStatus(s *rescanstat.Status) Option {
53+
return func(o *options) { o.status = s }
54+
}
55+
56+
func WithRescanLog(s *rescanlog.Sink) Option {
57+
return func(o *options) { o.rescanLog = s }
58+
}
59+
60+
type Loop struct {
3561
store *store.Store
3662
dir string
3763
bootstrapInterval time.Duration
@@ -50,22 +76,27 @@ type RescanLoop struct {
5076
onChange func()
5177
}
5278

53-
// SetChangeObserver sets a callback fired after a scan that mutated the store, set before Run (nil disables).
54-
func (r *RescanLoop) SetChangeObserver(fn func()) {
79+
func (r *Loop) SetChangeObserver(fn func()) {
5580
r.onChange = fn
5681
}
5782

58-
func (r *RescanLoop) notifyChange() {
83+
func (r *Loop) notifyChange() {
5984
if r.onChange != nil {
6085
r.onChange()
6186
}
6287
}
6388

64-
func NewRescanLoop(st *store.Store, dir string, interval time.Duration, cc config.CompileConfig, logger *slog.Logger, status *rescanstat.Status, rescanLog *rescanlog.Sink) (*RescanLoop, error) {
89+
func New(st *store.Store, dir string, interval time.Duration, opts ...Option) (*Loop, error) {
90+
var o options
91+
for _, opt := range opts {
92+
opt(&o)
93+
}
94+
6595
if interval <= 0 {
6696
interval = defaultRescanInterval
6797
}
68-
logger = loghelper.OrDiscard(logger)
98+
logger := loghelper.OrDiscard(o.logger)
99+
status := o.status
69100
if status == nil {
70101
status = rescanstat.New()
71102
}
@@ -75,7 +106,7 @@ func NewRescanLoop(st *store.Store, dir string, interval time.Duration, cc confi
75106
return nil, fmt.Errorf("failed to load: %s: %w", ignore.Path, err)
76107
}
77108

78-
return &RescanLoop{
109+
return &Loop{
79110
store: st,
80111
dir: dir,
81112
bootstrapInterval: interval,
@@ -87,16 +118,15 @@ func NewRescanLoop(st *store.Store, dir string, interval time.Duration, cc confi
87118
modTimes: make(map[string]time.Time),
88119
logger: logger,
89120
ignore: matcher,
90-
compileOpts: compiler.ConfigOptions(cc),
121+
compileOpts: compiler.ConfigOptions(o.compileConfig),
91122
status: status,
92-
rescanLog: rescanLog,
123+
rescanLog: o.rescanLog,
93124
}, nil
94125
}
95126

96-
func (r *RescanLoop) Run(ctx context.Context) {
127+
func (r *Loop) Run(ctx context.Context) {
97128
r.reloadConfig()
98129

99-
// Startup reconcile catches edits made between the last compile and now.
100130
if r.enabled {
101131
r.scan(ctx)
102132
}
@@ -122,8 +152,7 @@ func (r *RescanLoop) Run(ctx context.Context) {
122152
}
123153
}
124154

125-
// Re-source the rescan block from config.json.
126-
func (r *RescanLoop) reloadConfig() (intervalChanged bool) {
155+
func (r *Loop) reloadConfig() (intervalChanged bool) {
127156
path := filepath.Join(r.dir, config.DirName, config.FileName)
128157

129158
data, err := os.ReadFile(path)
@@ -167,7 +196,7 @@ func (r *RescanLoop) reloadConfig() (intervalChanged bool) {
167196
return intervalChanged
168197
}
169198

170-
func (r *RescanLoop) scan(ctx context.Context) {
199+
func (r *Loop) scan(ctx context.Context) {
171200
var changed []string
172201
seen := make(map[string]bool, len(r.modTimes))
173202
pending := make(map[string]time.Time)
@@ -221,7 +250,6 @@ func (r *RescanLoop) scan(ctx context.Context) {
221250

222251
mtime := info.ModTime()
223252

224-
// Skip files still settling.
225253
if now.Sub(mtime) < r.settle {
226254
return nil
227255
}
@@ -234,7 +262,6 @@ func (r *RescanLoop) scan(ctx context.Context) {
234262
return nil
235263
})
236264

237-
// Purge entries for deleted files only when the walk was complete.
238265
if walkErr != nil {
239266
r.logger.Error("rescan: walk aborted", "dir", r.dir, "err", walkErr)
240267
snap.Error = walkErr.Error()
@@ -255,7 +282,6 @@ func (r *RescanLoop) scan(ctx context.Context) {
255282
deleted = append(deleted, rel)
256283
}
257284

258-
// Lock only around store mutations.
259285
r.store.OpMu.Lock()
260286
defer r.store.OpMu.Unlock()
261287

@@ -297,8 +323,7 @@ func (r *RescanLoop) scan(ctx context.Context) {
297323
r.notifyChange()
298324
}
299325

300-
// Returns one PurgedFile per source file that lost nodes.
301-
func (r *RescanLoop) reconcileDeleted(ctx context.Context, deleted []string) []rescanstat.PurgedFile {
326+
func (r *Loop) reconcileDeleted(ctx context.Context, deleted []string) []rescanstat.PurgedFile {
302327
if len(deleted) == 0 {
303328
return nil
304329
}
@@ -323,7 +348,6 @@ func (r *RescanLoop) reconcileDeleted(ctx context.Context, deleted []string) []r
323348
OldContent: n.Content,
324349
})
325350

326-
// "rem:" prefix keeps the delete cursor-hash domain disjoint from compile's.
327351
synthetic = append(synthetic, &parser.ContextNode{ContentHash: "rem:" + n.ID + ":" + n.ContentHash})
328352
counts[n.SourceFile]++
329353
}

0 commit comments

Comments
 (0)