Skip to content

Commit 225fe95

Browse files
feat(parser): migrate db-backed providers
Move Forge, Piebald, and Warp DB discovery and per-session parse ownership onto their shared db-backed provider implementation, deleting the package-level legacy entrypoints and per-agent engine sync dispatch. The three become provider-authoritative. Full-sync change detection runs through syncProviderDBBackedAgent, which enumerates provider sources and skips those whose fingerprint mtime matches the stored data-version mtime, so a repeat sync of unchanged data stays a no-op. FindSourceFile, SourceMtime, and SyncSingleSession route these agents through the provider facade, preserving Piebald's chat-source-resolves-fork semantics including rejection of unknown forks. Assert the provider-authoritative skip in the Piebald process test: an unchanged chat skips on its per-chat updated_at fingerprint, matching the legacy piebaldPendingSessionIDs skip and the Forge sibling. The prior test asserted the opposite, a stale shadow-compare expectation that reparsed an untouched session on every full sync. refactor(parser): delete db-backed legacy whole-database parsers The db-backed provider migration left the exported whole-database and single-session parse free functions (ParseForgeDB, ParsePiebaldDB, ParsePiebaldSession, ParseWarpDB) in place: the provider routes through the lowercase per-session helpers (parseForgeSession, parsePiebaldSessionResults, parseWarpSession), so these survived only as dead production code kept alive by their own tests. ParsePiebaldDB had no references at all. Delete the four functions and the now-orphaned chain (loadForgeConversations, loadWarpConversations, loadPiebaldChats, and the ForgeSession/WarpSession/PiebaldSession bundle types). The retained parse tests now drive the provider facade (Discover + Fingerprint + Parse) via a shared parseDBBackedAll helper instead of the deleted free functions, so they exercise the production path. Extend the db-backed deletion guard to assert the four names stay gone. fix(parser): honor sqlite fanout watch roots SQLite fanout providers can publish a filesystem watch root that differs from the configured source root when FindDB resolves a canonical database under a subdirectory. Changed-path classification still compared WatchRoot to the configured root, so real DB/WAL/SHM events from the planned watch root produced no sources.\n\nAccept the emitted canonical DB directory as the matching watch root while keeping the configured-root compatibility path, and cover the FindDB subdirectory case with a WatchPlan-driven WAL event regression. The commit also removes two unused Codex fixture restats that blocked the existing staticcheck hook.\n\nValidation: go test -tags "fts5" ./internal/parser -run 'TestSQLiteFanoutSourceSet|TestDBBacked' -count=1 -v; go test -tags "fts5" ./internal/parser -count=1; go test -tags "fts5" ./internal/sync -run 'Test.*Codex' -count=1; go fmt ./...; go vet ./... style(docs): mdformat provider facade design spec
1 parent ad22cf2 commit 225fe95

17 files changed

Lines changed: 2522 additions & 1043 deletions

docs/superpowers/specs/2026-06-19-provider-facade-design.md

Lines changed: 198 additions & 60 deletions
Large diffs are not rendered by default.

internal/parser/db_backed_provider.go

Lines changed: 598 additions & 0 deletions
Large diffs are not rendered by default.

internal/parser/db_backed_provider_test.go

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.

internal/parser/forge.go

Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"database/sql"
55
"encoding/json"
66
"fmt"
7-
"log"
87
"os"
98
"path/filepath"
109
"strings"
@@ -15,12 +14,6 @@ import (
1514

1615
const forgeDBFilename = ".forge.db"
1716

18-
// ForgeSession bundles a parsed session with its messages.
19-
type ForgeSession struct {
20-
Session ParsedSession
21-
Messages []ParsedMessage
22-
}
23-
2417
// ForgeSessionMeta is lightweight metadata for a session,
2518
// used to detect changes without parsing messages.
2619
type ForgeSessionMeta struct {
@@ -29,8 +22,8 @@ type ForgeSessionMeta struct {
2922
FileMtime int64
3023
}
3124

32-
// FindForgeDBPath returns the Forge SQLite database path when present.
33-
func FindForgeDBPath(dir string) string {
25+
// forgeDBPath returns the Forge SQLite database path when present.
26+
func forgeDBPath(dir string) string {
3427
if dir == "" {
3528
return ""
3629
}
@@ -83,41 +76,8 @@ func ListForgeSessionMeta(dbPath string) ([]ForgeSessionMeta, error) {
8376
return metas, rows.Err()
8477
}
8578

86-
// ParseForgeDB opens the Forge SQLite database read-only and returns
87-
// all conversations with parsed messages.
88-
func ParseForgeDB(dbPath, machine string) ([]ForgeSession, error) {
89-
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
90-
return nil, nil
91-
}
92-
93-
db, err := openForgeDB(dbPath)
94-
if err != nil {
95-
return nil, err
96-
}
97-
defer db.Close()
98-
99-
convos, err := loadForgeConversations(db)
100-
if err != nil {
101-
return nil, fmt.Errorf("loading forge conversations: %w", err)
102-
}
103-
104-
var results []ForgeSession
105-
for _, c := range convos {
106-
parsed, msgs, err := buildForgeSession(c, dbPath, machine)
107-
if err != nil {
108-
log.Printf("forge conversation %s: %v", c.id, err)
109-
continue
110-
}
111-
if parsed == nil {
112-
continue
113-
}
114-
results = append(results, ForgeSession{Session: *parsed, Messages: msgs})
115-
}
116-
return results, nil
117-
}
118-
119-
// ParseForgeSession parses a single conversation by ID from the Forge database.
120-
func ParseForgeSession(dbPath, conversationID, machine string) (*ParsedSession, []ParsedMessage, error) {
79+
// parseForgeSession parses a single conversation by ID from the Forge database.
80+
func parseForgeSession(dbPath, conversationID, machine string) (*ParsedSession, []ParsedMessage, error) {
12181
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
12282
return nil, nil, fmt.Errorf("forge db not found: %s", dbPath)
12383
}
@@ -153,34 +113,6 @@ type forgeConversationRow struct {
153113
metrics string
154114
}
155115

156-
func loadForgeConversations(db *sql.DB) ([]forgeConversationRow, error) {
157-
rows, err := db.Query(`
158-
SELECT conversation_id,
159-
COALESCE(title, ''),
160-
COALESCE(context, ''),
161-
created_at,
162-
COALESCE(updated_at, created_at),
163-
COALESCE(metrics, '')
164-
FROM conversations
165-
WHERE context IS NOT NULL
166-
ORDER BY COALESCE(updated_at, created_at)
167-
`)
168-
if err != nil {
169-
return nil, err
170-
}
171-
defer rows.Close()
172-
173-
var convos []forgeConversationRow
174-
for rows.Next() {
175-
var c forgeConversationRow
176-
if err := rows.Scan(&c.id, &c.title, &c.context, &c.createdAt, &c.updatedAt, &c.metrics); err != nil {
177-
return nil, err
178-
}
179-
convos = append(convos, c)
180-
}
181-
return convos, rows.Err()
182-
}
183-
184116
func loadOneForgeConversation(db *sql.DB, conversationID string) (forgeConversationRow, error) {
185117
row := db.QueryRow(`
186118
SELECT conversation_id,

internal/parser/forge_test.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func TestParseForgeDB_StandardConversation(t *testing.T) {
166166
defer db.Close()
167167
seedForgeConversation(t, seeder)
168168

169-
sessions, err := ParseForgeDB(dbPath, "testmachine")
169+
sessions, err := parseForgeAll(dbPath, "testmachine")
170170
require.NoError(t, err, "ParseForgeDB")
171171

172172
assertEq(t, "sessions len", len(sessions), 1)
@@ -200,8 +200,8 @@ func TestParseForgeSession_SingleConversation(t *testing.T) {
200200
defer db.Close()
201201
seedForgeConversation(t, seeder)
202202

203-
sess, msgs, err := ParseForgeSession(dbPath, "conv-001", "testmachine")
204-
require.NoError(t, err, "ParseForgeSession")
203+
sess, msgs, err := parseForgeSession(dbPath, "conv-001", "testmachine")
204+
require.NoError(t, err, "parseForgeSession")
205205
require.NotNil(t, sess, "expected non-nil session")
206206

207207
assertEq(t, "ID", sess.ID, "forge:conv-001")
@@ -266,8 +266,8 @@ func TestCollectForgeToolCalls_TaskSubagentIDPrefixed(t *testing.T) {
266266
"2026-05-02 10:00:00", "2026-05-02 10:00:01", "",
267267
)
268268

269-
sess, msgs, err := ParseForgeSession(dbPath, "parent-conv", "m")
270-
require.NoError(t, err, "ParseForgeSession")
269+
sess, msgs, err := parseForgeSession(dbPath, "parent-conv", "m")
270+
require.NoError(t, err, "parseForgeSession")
271271
require.NotNil(t, sess, "expected non-nil session")
272272
require.NotEmpty(t, msgs, "expected messages")
273273
var taskCall *ParsedToolCall
@@ -282,13 +282,13 @@ func TestCollectForgeToolCalls_TaskSubagentIDPrefixed(t *testing.T) {
282282
assertEq(t, "SubagentSessionID", taskCall.SubagentSessionID, "forge:child-conv-001")
283283
}
284284

285-
func TestFindForgeDBPath(t *testing.T) {
285+
func TestForgeDBPath(t *testing.T) {
286286
dir := t.TempDir()
287-
assertEq(t, "not found", FindForgeDBPath(dir), "")
287+
assertEq(t, "not found", forgeDBPath(dir), "")
288288

289289
dbPath, _, db := newForgeTestDB(t)
290290
defer db.Close()
291-
assertEq(t, "found", FindForgeDBPath(filepath.Dir(dbPath)), dbPath)
291+
assertEq(t, "found", forgeDBPath(filepath.Dir(dbPath)), dbPath)
292292
}
293293

294294
// ---------------------------------------------------------------------------
@@ -341,7 +341,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
341341
"", // empty metrics → accumulateMessageTokenUsage fallback
342342
)
343343

344-
sessions, err := ParseForgeDB(dbPath, "m")
344+
sessions, err := parseForgeAll(dbPath, "m")
345345
require.NoError(t, err, "ParseForgeDB")
346346
require.Len(t, sessions, 1)
347347
s := sessions[0].Session
@@ -387,7 +387,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
387387
`{"output_tokens": 42}`,
388388
)
389389

390-
sessions, err := ParseForgeDB(dbPath, "m")
390+
sessions, err := parseForgeAll(dbPath, "m")
391391
require.NoError(t, err, "ParseForgeDB")
392392
require.Len(t, sessions, 1)
393393
s := sessions[0].Session
@@ -426,7 +426,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
426426
"",
427427
)
428428

429-
sessions, err := ParseForgeDB(dbPath, "m")
429+
sessions, err := parseForgeAll(dbPath, "m")
430430
require.NoError(t, err, "ParseForgeDB")
431431
require.Len(t, sessions, 1)
432432
msgs := sessions[0].Messages
@@ -460,7 +460,7 @@ func TestForgeTokenFallbacks(t *testing.T) {
460460
"",
461461
)
462462

463-
sessions, err := ParseForgeDB(dbPath, "m")
463+
sessions, err := parseForgeAll(dbPath, "m")
464464
require.NoError(t, err, "ParseForgeDB")
465465
require.Len(t, sessions, 1)
466466
msgs := sessions[0].Messages
@@ -514,7 +514,7 @@ func TestForgeDegenerate(t *testing.T) {
514514
"",
515515
)
516516

517-
sessions, err := ParseForgeDB(dbPath, "m")
517+
sessions, err := parseForgeAll(dbPath, "m")
518518
require.NoError(t, err, "ParseForgeDB")
519519
require.Len(t, sessions, 1)
520520
// Only the user message; empty assistant was skipped.
@@ -556,7 +556,7 @@ func TestForgeDegenerate(t *testing.T) {
556556
"",
557557
)
558558

559-
sessions, err := ParseForgeDB(dbPath, "m")
559+
sessions, err := parseForgeAll(dbPath, "m")
560560
require.NoError(t, err, "ParseForgeDB")
561561
require.Len(t, sessions, 1)
562562
// Only the user message; tool result with empty call_id was skipped.
@@ -589,7 +589,7 @@ func TestForgeDegenerate(t *testing.T) {
589589
"",
590590
)
591591

592-
sessions, err := ParseForgeDB(dbPath, "m")
592+
sessions, err := parseForgeAll(dbPath, "m")
593593
require.NoError(t, err, "ParseForgeDB")
594594
require.Len(t, sessions, 1)
595595
msgs := sessions[0].Messages
@@ -628,7 +628,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
628628
"",
629629
)
630630

631-
sessions, err := ParseForgeDB(dbPath, "m")
631+
sessions, err := parseForgeAll(dbPath, "m")
632632
require.NoError(t, err, "ParseForgeDB")
633633
require.Len(t, sessions, 1)
634634
s := sessions[0].Session
@@ -670,7 +670,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
670670
"",
671671
)
672672

673-
sessions, err := ParseForgeDB(dbPath, "m")
673+
sessions, err := parseForgeAll(dbPath, "m")
674674
require.NoError(t, err, "ParseForgeDB")
675675
require.Len(t, sessions, 1)
676676
assertEq(t, "Cwd", sessions[0].Session.Cwd, "")
@@ -710,7 +710,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
710710
"",
711711
)
712712

713-
sessions, err := ParseForgeDB(dbPath, "m")
713+
sessions, err := parseForgeAll(dbPath, "m")
714714
require.NoError(t, err, "ParseForgeDB")
715715
require.Len(t, sessions, 1)
716716
assertEq(t, "Cwd", sessions[0].Session.Cwd, "")
@@ -741,7 +741,7 @@ func TestForgeCwdEdgeCases(t *testing.T) {
741741
"",
742742
)
743743

744-
sessions, err := ParseForgeDB(dbPath, "m")
744+
sessions, err := parseForgeAll(dbPath, "m")
745745
require.NoError(t, err, "ParseForgeDB")
746746
require.Len(t, sessions, 1)
747747
s := sessions[0].Session
@@ -814,7 +814,7 @@ func TestForgeEndedAtFallback(t *testing.T) {
814814
_, err := db.Exec("UPDATE conversations SET updated_at = NULL WHERE conversation_id = 'ended-fallback'")
815815
require.NoError(t, err, "update")
816816

817-
sessions, err := ParseForgeDB(dbPath, "m")
817+
sessions, err := parseForgeAll(dbPath, "m")
818818
require.NoError(t, err, "ParseForgeDB")
819819
require.Len(t, sessions, 1)
820820
s := sessions[0].Session
@@ -864,7 +864,7 @@ func TestForgeToolOutputText(t *testing.T) {
864864
"",
865865
)
866866

867-
sessions, err := ParseForgeDB(dbPath, "m")
867+
sessions, err := parseForgeAll(dbPath, "m")
868868
require.NoError(t, err, "ParseForgeDB")
869869
require.Len(t, sessions, 1)
870870
msgs := sessions[0].Messages
@@ -936,7 +936,7 @@ func TestForgeSkillToolName(t *testing.T) {
936936
"",
937937
)
938938

939-
sessions, err := ParseForgeDB(dbPath, "m")
939+
sessions, err := parseForgeAll(dbPath, "m")
940940
require.NoError(t, err, "ParseForgeDB")
941941
require.Len(t, sessions, 1)
942942
var skillCall *ParsedToolCall
@@ -994,7 +994,7 @@ func TestForgeReasoningNoText(t *testing.T) {
994994
"",
995995
)
996996

997-
sessions, err := ParseForgeDB(dbPath, "m")
997+
sessions, err := parseForgeAll(dbPath, "m")
998998
require.NoError(t, err, "ParseForgeDB")
999999
require.Len(t, sessions, 1)
10001000
msgs := sessions[0].Messages

0 commit comments

Comments
 (0)