Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 4 additions & 4 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# The compatibility bundle is a byte-for-byte oracle, not a text file.
document/internal/compattest/testdata/document-compat-v1.json -text
# The evidence golden bundle is a byte-for-byte oracle, not a text file.
# Document goldens are byte-for-byte protocol, rendering, and vector oracles.
document/testdata/normalized-evidence-v1.golden.json -text
# The rendition golden bundle is a byte-for-byte oracle, not a text file.
document/testdata/rendition-v1.golden.md -text
# The processing profile golden bundle is a byte-for-byte oracle, not a text file.
document/testdata/profile-v1.golden.json -text
document/testdata/rendition-v1.golden.md -text
document/testdata/source-metadata-v1.golden.json -text
document/testdata/vector-set-v1.golden.bin -text
10 changes: 10 additions & 0 deletions .roborev.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
agent = "pi"
model = "zai/glm-5.3"
review_agent = "pi"
review_model = "zai/glm-5.3"
review_reasoning = "high"

review_guidelines = """
docbank is a single-user personal document archive: a local daemon owns
the vault (SQLite + content-addressed blobs) and the CLI, agents, and a
Expand Down Expand Up @@ -138,3 +144,7 @@ Do NOT flag issues that only apply to public-facing, multi-tenant, or
internet-exposed services. Focus on bugs, logic errors, data corruption
risks, and code quality issues.
"""

# Cadence: reviews are batched, not per-commit. No post-commit hook is
# installed; every 5 commits on a working branch, run a range review:
# roborev review <last-reviewed-sha>..HEAD
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ install-hooks:
docs-install:
cd docs && uv sync --frozen

docs-build:
bridge-contract:
go test -tags fts5 ./document/bridge -run '^TestBridgeContractNormativeDocuments'

docs-build: bridge-contract
cd docs && ./zensical-docs.sh build

docs-serve:
Expand Down
56 changes: 56 additions & 0 deletions cmd/docbank/backfill_retry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import "time"

const maxBackfillRetryDelay = 10 * time.Minute

type backfillRetryState struct {
failures uint
notBefore time.Time
}

type backfillRetrySet map[string]backfillRetryState

func newBackfillRetrySet() backfillRetrySet {
return make(backfillRetrySet)
}

func (retries backfillRetrySet) ready(key string, now time.Time) bool {
state, found := retries[key]
return !found || !now.Before(state.notBefore)
}

func (retries backfillRetrySet) failed(key string, now time.Time) {
state := retries[key]
state.failures++
delay := 5 * time.Second
for attempt := uint(1); attempt < state.failures && delay < maxBackfillRetryDelay; attempt++ {
delay = min(delay*2, maxBackfillRetryDelay)
}
state.notBefore = now.Add(delay)
retries[key] = state
}

func (retries backfillRetrySet) succeeded(key string) {
delete(retries, key)
}

func (retries backfillRetrySet) waitDelay(now time.Time, maximum time.Duration) time.Duration {
delay := maximum
for _, state := range retries {
if !now.Before(state.notBefore) {
return 0
}
delay = min(delay, state.notBefore.Sub(now))
}
return delay
}

func backfillBatchWaitDelay(
attempted int, retries backfillRetrySet, now time.Time,
) time.Duration {
if attempted != 0 || len(retries) == 0 {
return 100 * time.Millisecond
}
return retries.waitDelay(now, 10*time.Second)
}
38 changes: 38 additions & 0 deletions cmd/docbank/backfill_retry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestBackfillRetrySetQuarantinesFailuresWithoutBlockingOtherTargets(t *testing.T) {
retries := newBackfillRetrySet()
now := time.Date(2026, time.August, 24, 12, 0, 0, 0, time.UTC)
assert.True(t, retries.ready("bad", now))
assert.True(t, retries.ready("later", now))

retries.failed("bad", now)
assert.False(t, retries.ready("bad", now))
assert.True(t, retries.ready("later", now))
assert.True(t, retries.ready("bad", now.Add(5*time.Second)))

retries.failed("bad", now.Add(5*time.Second))
assert.False(t, retries.ready("bad", now.Add(14*time.Second)))
assert.True(t, retries.ready("bad", now.Add(15*time.Second)))

retries.succeeded("bad")
assert.True(t, retries.ready("bad", now))
}

func TestBackfillBatchWaitHonorsRetryWhenEveryTargetIsSkipped(t *testing.T) {
retries := newBackfillRetrySet()
now := time.Date(2026, time.August, 25, 12, 0, 0, 0, time.UTC)
retries.failed("deferred", now)

assert.Equal(t, 5*time.Second,
backfillBatchWaitDelay(0, retries, now))
assert.Equal(t, 100*time.Millisecond,
backfillBatchWaitDelay(1, retries, now))
}
40 changes: 30 additions & 10 deletions cmd/docbank/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,13 +686,25 @@ func TestJobsShowsDaemonStatus(t *testing.T) {
assert.Contains(t, out, "extract:plain-text")
assert.Contains(t, out, "running")

out, err = runCLI(t, "jobs", "--json")
require.NoError(t, err)
var got api.JobList
require.NoError(t, json.Unmarshal([]byte(out), &got))
require.Len(t, got.Items, 1)
require.Eventually(t, func() bool {
out, err = runCLI(t, "jobs", "--json")
if err != nil || json.Unmarshal([]byte(out), &got) != nil || len(got.Items) != 4 {
return false
}
return got.Items[1].Name == "extract:source-metadata" &&
got.Items[2].Name == "maintenance:auxiliary-checksums" && got.Items[2].Status == "completed" &&
got.Items[3].Name == "process:vector-indexes" && got.Items[3].Status == "running"
}, 5*time.Second, 25*time.Millisecond)
require.Len(t, got.Items, 4)
assert.Equal(t, "extract:plain-text", got.Items[0].Name)
assert.Equal(t, "running", got.Items[0].Status)
assert.Equal(t, "extract:source-metadata", got.Items[1].Name)
assert.Equal(t, "running", got.Items[1].Status)
assert.Equal(t, "maintenance:auxiliary-checksums", got.Items[2].Name)
assert.Equal(t, "completed", got.Items[2].Status)
assert.Equal(t, "process:vector-indexes", got.Items[3].Name)
assert.Equal(t, "running", got.Items[3].Status)
}

func TestConfiguredAutomaticPackingPacksAndKeepsDaemonAlive(t *testing.T) {
Expand Down Expand Up @@ -734,10 +746,14 @@ func TestConfiguredAutomaticPackingPacksAndKeepsDaemonAlive(t *testing.T) {
require.NoError(t, err)
var got api.JobList
require.NoError(t, json.Unmarshal([]byte(out), &got))
require.Len(t, got.Items, 2)
require.Len(t, got.Items, 5)
assert.Equal(t, "extract:plain-text", got.Items[0].Name)
assert.Equal(t, "storage:pack", got.Items[1].Name)
assert.Equal(t, "running", got.Items[1].Status)
assert.Equal(t, "extract:source-metadata", got.Items[1].Name)
assert.Equal(t, "maintenance:auxiliary-checksums", got.Items[2].Name)
assert.Equal(t, "process:vector-indexes", got.Items[3].Name)
assert.Equal(t, "running", got.Items[3].Status)
assert.Equal(t, "storage:pack", got.Items[4].Name)
assert.Equal(t, "running", got.Items[4].Status)

time.Sleep(100 * time.Millisecond)
_, _, found, err := client.Find(t.Context(), home)
Expand Down Expand Up @@ -778,11 +794,15 @@ func TestConfiguredWatchIngestsStableFilesAndRemainsObservable(t *testing.T) {
require.NoError(t, err)
var got api.JobList
require.NoError(t, json.Unmarshal([]byte(out), &got))
require.Len(t, got.Items, 2)
require.Len(t, got.Items, 5)
assert.Equal(t, "extract:plain-text", got.Items[0].Name)
assert.Equal(t, "running", got.Items[0].Status)
assert.Equal(t, "watch:sessions", got.Items[1].Name)
assert.Equal(t, "running", got.Items[1].Status)
assert.Equal(t, "extract:source-metadata", got.Items[1].Name)
assert.Equal(t, "maintenance:auxiliary-checksums", got.Items[2].Name)
assert.Equal(t, "process:vector-indexes", got.Items[3].Name)
assert.Equal(t, "running", got.Items[3].Status)
assert.Equal(t, "watch:sessions", got.Items[4].Name)
assert.Equal(t, "running", got.Items[4].Status)

out, err = runCLI(t, "watch", "list", "--json")
require.NoError(t, err)
Expand Down
Loading
Loading