diff --git a/AGENTS.md b/AGENTS.md index 3ab90d6..e7ab45d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,11 +36,9 @@ PLAN.md decisions + execution + open questions docs/ published docs skills/ in-project agent skills (vendored, no external links) bin/ self-describing tools bin/{subject}/{method}.go (shebang) -bin/brain/ search.go (deduction); libs in internal/brain +bin/brain/ search.go, serve.go; libs in internal/brain and internal/httpapi internal/ shared Go (brain/rank is cgo-free) -bin/serve.go async Go HTTP server entry (self-executing go run shebang) -bin/watch/ corpus watcher Go package (mtimes, no inotify deps) -bin/server/ async Go HTTP server (goroutines, bounded worker pool) +bin/watch/ corpus watcher (internal via bin/brain/watch later) bin/mail/ mail pipeline: sync (Go), import (md), index_mail (rebuild) bin/tools/ vendored python libs behind bin/* (kblib, yamlout, websearch) bin/docker-entrypoint container entrypoint (brain index|search|serve|watch) diff --git a/bin/brain/serve.go b/bin/brain/serve.go new file mode 100755 index 0000000..03be4c6 --- /dev/null +++ b/bin/brain/serve.go @@ -0,0 +1,26 @@ +//usr/bin/env go run -tags=brain_serve "$0" "$@"; exit +//go:build brain_serve +// +// bin/brain/serve.go - HTTP API for the 2dph brain. +// +// KB_ROOT=/path/to/2dph ./bin/brain/serve.go +// KB_SEARCH_CMD=... KB_WORKERS=4 KB_PORT=8630 ./bin/brain/serve.go +// +// Default search backend is var/bin/brain-search (Go), not Python. +// NOTE: never run `gofmt -w` on this file — it breaks the shebang. +package main + +import ( + "os" + + "github.com/eSlider/2dph/internal/httpapi" +) + +func main() { + if os.Getenv("KB_ROOT") == "" { + if wd, err := os.Getwd(); err == nil { + os.Setenv("KB_ROOT", wd) + } + } + httpapi.Run() +} diff --git a/bin/doc.go b/bin/doc.go new file mode 100644 index 0000000..e8d054b --- /dev/null +++ b/bin/doc.go @@ -0,0 +1,2 @@ +// Deprecated shebang mains at bin root (serve.go is tagged brain_serve). +package main diff --git a/bin/serve.go b/bin/serve.go index ada62bc..3feaa3d 100755 --- a/bin/serve.go +++ b/bin/serve.go @@ -1,27 +1,22 @@ -//usr/bin/env go run "$0" "$@"; exit -// bin/serve.go - async Go HTTP server for the 2dph brain (see bin/server). +//usr/bin/env go run -tags=brain_serve "$0" "$@"; exit +//go:build brain_serve // -// KB_ROOT=/path/to/2dph ./bin/serve.go # serve the brain -// KB_SEARCH_CMD=... KB_WORKERS=4 KB_PORT=8630 ./bin/serve.go -// -// Shebang trick: the first line is a Go `//` comment; when executed, env runs -// `go run "$0"` so this file doubles as an executable script. The real code -// lives in the importable package (module path, never a relative import). -// NOTE: never run `gofmt -w` on this file - it rewrites `//usr/bin/env` to -// `// usr/...` and breaks the shebang. +// bin/serve.go — deprecated; use bin/brain/serve.go. package main import ( + "fmt" "os" - "github.com/eSlider/2dph/bin/server" + "github.com/eSlider/2dph/internal/httpapi" ) func main() { - if env := os.Getenv("KB_ROOT"); env == "" { + fmt.Fprintln(os.Stderr, "bin/serve.go is deprecated; use bin/brain/serve.go") + if os.Getenv("KB_ROOT") == "" { if wd, err := os.Getwd(); err == nil { os.Setenv("KB_ROOT", wd) } } - server.Run() + httpapi.Run() } diff --git a/bin/server/server.go b/internal/httpapi/server.go similarity index 82% rename from bin/server/server.go rename to internal/httpapi/server.go index 9e23824..e190014 100644 --- a/bin/server/server.go +++ b/internal/httpapi/server.go @@ -2,15 +2,10 @@ // // Async by design: every request runs on its own goroutine, and CPU-heavy // searches are serialized through a bounded worker pool (a counting -// semaphore) so N requests can't spawn N Python interpreters at once. +// semaphore) so N requests can't spawn N search processes at once. // -// Used by bin/serve.go which is a self-executing shebang script: -// -// ///usr/bin/env go run "$0" "$@"; exit -// package main -// import "github.com/eSlider/2dph/bin/server" -// func main() { server.Run() } -package server +// Used by bin/brain/serve.go. +package httpapi import ( "context" @@ -100,8 +95,8 @@ func writeRaw(w http.ResponseWriter, code int, body []byte) { w.Write(body) } -// brainSearcher shells out to bin/kb/search --json. A single python search -// is bounded and short-lived; the worker pool keeps at most N live. +// brainSearcher shells out to the Go brain-search binary (not Python). +// A single search is bounded and short-lived; the worker pool keeps at most N live. type brainSearcher struct { cmdPath string timeout time.Duration @@ -122,15 +117,18 @@ func (b *brainSearcher) Search(ctx context.Context, query string, limit int) ([] return out, nil } -// Run starts the HTTP server. Reads env: KB_SEARCH_CMD (default bin/kb/search, -// relative to the repo root given by KB_ROOT), KB_WORKERS (default 4), KB_PORT -// (default 8630). +func defaultSearchCmd(root string) string { + if env := os.Getenv("KB_SEARCH_CMD"); env != "" { + return env + } + return filepath.Join(root, "var", "bin", "brain-search") +} + +// Run starts the HTTP server. Reads env: KB_SEARCH_CMD (default +// $KB_ROOT/var/bin/brain-search), KB_WORKERS (default 4), KB_PORT (default 8630). func Run() { root := os.Getenv("KB_ROOT") - searchPath := os.Getenv("KB_SEARCH_CMD") - if searchPath == "" { - searchPath = filepath.Join(root, "bin", "kb", "search") - } + searchPath := defaultSearchCmd(root) workers := 4 if raw := os.Getenv("KB_WORKERS"); raw != "" { if n, err := strconv.Atoi(raw); err == nil && n > 0 { @@ -147,7 +145,7 @@ func Run() { searcher := &brainSearcher{cmdPath: searchPath, timeout: 60 * time.Second} handler := NewServer(searcher, workers) addr := "127.0.0.1:" + strconv.Itoa(port) - log.Printf("serve: %s (workers=%d)", addr, workers) + log.Printf("serve: %s (workers=%d cmd=%s)", addr, workers, searchPath) if err := http.ListenAndServe(addr, handler); err != nil { log.Fatal(err) } diff --git a/bin/server/server_test.go b/internal/httpapi/server_test.go similarity index 91% rename from bin/server/server_test.go rename to internal/httpapi/server_test.go index da5641e..2e226a4 100644 --- a/bin/server/server_test.go +++ b/internal/httpapi/server_test.go @@ -1,10 +1,11 @@ -package server +package httpapi import ( "context" "encoding/json" "net/http" "net/http/httptest" + "strings" "sync" "sync/atomic" "testing" @@ -141,6 +142,17 @@ func TestSearchRejectsBadLimit(t *testing.T) { } } +func TestDefaultSearchCmdIsBrainNotPython(t *testing.T) { + t.Setenv("KB_SEARCH_CMD", "") + cmd := defaultSearchCmd("/repo") + if strings.Contains(strings.ToLower(cmd), "python") { + t.Fatalf("search path still python: %s", cmd) + } + if !strings.Contains(cmd, "brain") { + t.Fatalf("search path must be the Go brain binary, got %s", cmd) + } +} + func TestSearchTimeout(t *testing.T) { fs := &fakeSearcher{delay: time.Second} h := NewServer(fs, 1)