feat: D24 fact intervals (--as-of) and bin/stack assistant helpers. (#37)
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) 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:
@@ -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>" {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user