feat: D24 fact intervals (--as-of) and bin/stack assistant helpers.
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (pull_request) Failing after 5s
Tests / OCR (tesseract fixture) (pull_request) Failing after 4s
Tests / Release (semver) (pull_request) Skipped

Store valid_from/valid_to on leafs and filter search by calendar day without
overloading D16 source staleness; stack start/start-assistant wires brain + PicoClaw.
This commit is contained in:
2026-08-14 15:45:09 +01:00
parent 0c4bb87001
commit 094e2cb9df
34 changed files with 991 additions and 47 deletions
+8 -1
View File
@@ -105,11 +105,18 @@ func (s *Server) mcpCall(r *http.Request, params json.RawMessage) (any, error) {
if limit < 1 || limit > 100 {
return mcpText(`{"error":"n must be int 1..100"}`, true), nil
}
asOf := ""
if raw, ok := p.Arguments["as_of"]; ok {
asOf = strings.TrimSpace(fmt.Sprint(raw))
if asOf == "<nil>" {
asOf = ""
}
}
if !s.tryAcquire(r) {
return nil, fmt.Errorf("cancelled")
}
defer s.release()
body, err = s.api.Search(r.Context(), q, limit)
body, err = s.api.Search(r.Context(), q, limit, asOf)
case "get":
id := strings.TrimSpace(fmt.Sprint(p.Arguments["id"]))
if id == "" || id == "<nil>" {
+10 -4
View File
@@ -24,7 +24,7 @@ import (
// API is the in-process brain surface. Production serve.go wires internal/brain.
type API interface {
Search(ctx context.Context, query string, limit int) ([]byte, error)
Search(ctx context.Context, query string, limit int, asOf string) ([]byte, error)
Get(ctx context.Context, id string, body bool) ([]byte, error)
Stats(ctx context.Context) ([]byte, error)
Audit(ctx context.Context) ([]byte, error)
@@ -85,11 +85,12 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
}
limit = n
}
asOf := strings.TrimSpace(r.URL.Query().Get("as_of"))
if !s.acquire(w, r) {
return
}
defer s.release()
body, err := s.api.Search(r.Context(), q, limit)
body, err := s.api.Search(r.Context(), q, limit, asOf)
writeAPI(w, body, err)
}
@@ -187,13 +188,18 @@ type ExecSearcher struct {
Timeout time.Duration
}
func (b ExecSearcher) Search(ctx context.Context, query string, limit int) ([]byte, error) {
func (b ExecSearcher) Search(ctx context.Context, query string, limit int, asOf string) ([]byte, error) {
if b.Timeout == 0 {
b.Timeout = 60 * time.Second
}
ctx, cancel := context.WithTimeout(ctx, b.Timeout)
defer cancel()
cmd := exec.CommandContext(ctx, b.CmdPath, "--json", "-n", strconv.Itoa(limit), query)
args := []string{"--json", "-n", strconv.Itoa(limit)}
if asOf != "" {
args = append(args, "--as-of", asOf)
}
args = append(args, query)
cmd := exec.CommandContext(ctx, b.CmdPath, args...)
out, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
+5 -5
View File
@@ -21,10 +21,10 @@ type fakeSearcher struct {
calls int
active atomic.Int32
maxSeen atomic.Int32
callback func(q string, limit int) ([]byte, error)
callback func(q string, limit int, asOf string) ([]byte, error)
}
func (f *fakeSearcher) Search(ctx context.Context, query string, limit int) ([]byte, error) {
func (f *fakeSearcher) Search(ctx context.Context, query string, limit int, asOf string) ([]byte, error) {
f.mu.Lock()
f.calls++
f.mu.Unlock()
@@ -44,7 +44,7 @@ func (f *fakeSearcher) Search(ctx context.Context, query string, limit int) ([]b
}
}
if f.callback != nil {
return f.callback(query, limit)
return f.callback(query, limit, asOf)
}
return []byte(`{"query":"` + query + `","count":0,"results":[]}`), nil
}
@@ -109,7 +109,7 @@ func TestSearchMissingQuery(t *testing.T) {
}
func TestSearchReturnsSearcherResult(t *testing.T) {
fs := &fakeSearcher{callback: func(q string, limit int) ([]byte, error) {
fs := &fakeSearcher{callback: func(q string, limit int, asOf string) ([]byte, error) {
return []byte(`{"query":"` + q + `","count":1,"results":[{"id":"x"}]}`), nil
}}
h := NewServer(fs, 1)
@@ -168,7 +168,7 @@ func TestSearchRejectsBadLimit(t *testing.T) {
}
func TestGetLeaf(t *testing.T) {
fs := &fakeSearcher{callback: func(q string, limit int) ([]byte, error) {
fs := &fakeSearcher{callback: func(q string, limit int, asOf string) ([]byte, error) {
return []byte(`{}`), nil
}}
h := NewServer(fs, 1)
+3
View File
@@ -35,6 +35,7 @@ var Ops = []Op{
Params: []Param{
{Name: "q", In: "query", Type: "string", Description: "search query", Required: true},
{Name: "n", In: "query", Type: "integer", Description: "hit limit 1..100 (default 10)"},
{Name: "as_of", In: "query", Type: "string", Description: "YYYY-MM-DD; keep facts active on that day (D24)"},
},
},
{
@@ -54,6 +55,8 @@ var Ops = []Op{
{Name: "text", In: "query", Type: "string", Description: "leaf text (omit for CLI hint)"},
{Name: "root", In: "query", Type: "string", Description: "facts or info (default info)"},
{Name: "source", In: "query", Type: "string", Description: "evidence pointer; facts need two sources"},
{Name: "valid_from", In: "query", Type: "string", Description: "fact interval start YYYY-MM-DD (D24)"},
{Name: "valid_to", In: "query", Type: "string", Description: "fact interval end YYYY-MM-DD inclusive (D24)"},
},
},
{Path: PathOpenAPI, Method: "get", ID: "openapi", Summary: "OpenAPI 3 document for this server"},