Skip to content
Closed
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ jobs:
bash -n bin/db/psql-yq
bash -n bin/db/ssh-tunnel
bash -n bin/docker-entrypoint
bash -n bin/kb/search
bash -n bin/cgo/zig
sh -n bin/cgo/zcc
sh -n bin/cgo/zc++
Expand Down
22 changes: 16 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,30 @@ Read first: [PLAN](PLAN.md) → [docs](docs/) → [roadmap](docs/roadmap.md)
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 serve.go index.go add.go get.go stats.go eval.go watch.go

**Tool naming (D14, apply ALWAYS):** `bin/{subject}/{verb}-{object}.go` — singular,
**no trailing "s"**. `subject` = the object/system you act on; `verb` = the
action (import/write/read/list/sync…); `object` = what (last token, `-`-joined).
You read the path and know what it does. Examples: importing contacts into
OnlyOffice → `bin/onlyoffice/import-contact.go`; reading contacts → `bin/contact/list.go --csv`.
Never `contacts/…`, never `brainwrite.go` (that is `bin/brain/import-contact.go`).
Same rule for `var/` and `etc/` subdirs: `var/{subject}/…`, `etc/{subject}/…`.

bin/ self-describing tools (singular, `{subject}/{verb}-{object}.go`)
bin/brain/ import-contact.go search.go serve.go index.go add.go get.go stats.go eval.go watch.go
bin/contact/ import.go list.go (read/normalize csv/vcf/mab → stdout/file)
bin/onlyoffice/ import-contact.go (reconcile contacts into OO CRM)
bin/chats/ sync.go import.go facts.go apply.go; libs in internal/chats
bin/mail/ sync.go import.go ocr.go
bin/markdown/ import.go (H2 leaf split)
bin/postgres/ query.go (read-only YAML)
bin/git/ import.go (go-git history)
bin/web/ search.go (SearXNG)
bin/reasoner/ bakeoff.go (D18 CPU OpenAI tool-call bake-off)
internal/ shared Go (brain/rank is cgo-free; facts D16; cli flaggy D23; chats; gitlog; websearch; reasoner; duckstats)
pkg/ public reusable Go (cli flaggy D23; cmdbin; contact; duckstats; httpapi) — no 2dph deps
internal/ private 2dph Go (brain/rank cgo-free; facts D16; chats; gitlog; websearch; reasoner)
bin/qa/ stats.go (DuckDB quantiles / JSONL count; gcc CGO, not Zig)
bin/watch/ corpus watcher (used by bin/brain/watch.go)
bin/tools/ web-search fixtures (Go testdata for internal/websearch)
bin/cgo/ zig zcc zc++ (CGO via zig cc, not gcc)
bin/stack/ start start-assistant stop status (compose + PicoClaw agent)
bin/docker-entrypoint container entrypoint (api: serve|search|watch|index|add|mail-sync)
Expand Down Expand Up @@ -95,7 +106,6 @@ bin/stack/start-mail-sync # compos
```bash
bin/facts/audit.go ["self"|"db"|"contradict"] # 2-source + D16 adjudication
bin/facts/crm.go [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
bin/kb/search "query" [--repo X] # deprecated wrapper → bin/brain/search.go
bin/brain/search.go "query" [--root facts|info] # deduction search → YAML
bin/brain/search.go "query" --as-of 2025-01-01 # D24 fact intervals
bin/brain/search.go "query" --no-web # local graph only
Expand Down Expand Up @@ -126,7 +136,7 @@ bin/brain/deduce "question" # thinking wrapper
Never start a shell command with `cd` — use the tool working-directory
parameter. Search before reading whole files. For YAML/JSON/XML/CSV/TOML/HCL
prefer mikefarah/yq (`skills/yq/SKILL.md`). For bulk rows and quantiles use
duckdb-go (`internal/duckstats`, `skills/duckdb/SKILL.md`), not Ladybug.
duckdb-go (`pkg/duckstats`, `skills/duckdb/SKILL.md`), not Ladybug.

## GitHub safety rules (ABSOLUTE — never violate)

Expand Down
2 changes: 1 addition & 1 deletion PERF.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ full and no memory could be freed!`) and **every** endpoint — even `/stats`
Fixed in `internal/brain/*.go`: `defer res.Close()` on every QueryResult
(read + one-shot statements via `qClose`). Regression:
`TestConcurrentSearchesDontExhaustBufferPool` (red without the fix).
Live proof: `qa/stress` at c=8/16 sustained now holds 0% errors.
Live proof: `test/stress` at c=8/16 sustained now holds 0% errors.

## Tuning

Expand Down
46 changes: 25 additions & 21 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ detective method: **a fact needs ≥2 independent sources or it is
| D17 | assertion gate | Fact-check every *claim* (facts → info → live → web), not every edit. `bin/brain/search.go` adds a `web` block when there is no facts hit (`throttled`/`skipped`/`refused` ≠ absence). `--root` and `--no-web` stay local. Missing graph ≠ “does not exist”. |
| D18 | reasoner | Pluggable OpenAI-compatible URL (`REASONER_BASE_URL`). RAM: `Qwen/Qwen3.5-9B`. Quality: `prism-ml/Bonsai-27B-gguf` or `Qwen/Qwen3.6-27B`. No official Qwen3.6-9B. CPU bake-off: `bin/reasoner/bakeoff.go` + compose profile `reasoner` (`OLLAMA_NUM_GPU=0`, `:11435`). PicoClaw is compose profile `picoclaw`; tools are `search`/`get`/`audit`. Weights are not copied into the 2dph image. Agent lever/loop: [#15](https://git.produktor.io/eSlider/2dph/issues/15). |
| D19 | git history | [go-git](https://github.com/go-git/go-git) via `bin/git/import.go`. No subprocess of the git binary. Conversion prints commit leafs; brain write is `bin/brain/index.go`. |
| D20 | agent API | OpenAPI + MCP are generated from the same `internal/httpapi.Ops` table as `bin/brain/serve.go` handlers. `GET /openapi.json`, `POST /mcp` (JSON-RPC tools/list + tools/call). Tool names match OpenAPI paths (`search`/`get`/`stats`/`audit`/`ingest`). |
| D20 | agent API | OpenAPI + MCP are generated from the same `pkg/httpapi.Ops` table as `bin/brain/serve.go` handlers. `GET /openapi.json`, `POST /mcp` (JSON-RPC tools/list + tools/call). Tool names match OpenAPI paths (`search`/`get`/`stats`/`audit`/`ingest`). |
| D21 | CGO | Ladybug/tokenizers CGO is compiled with **Zig** (`bin/cgo/zcc` → `zig cc -target …-linux-gnu`), not gcc. `bin/cgo/zig` pins Zig 0.14.1 + liblbug 0.19.1 + libtokenizers 1.27.0. Compose `target: api` has no CPython; write/rebuild is profile `index`. |
| D22 | analytics | **duckdb-go** in-process (`internal/duckstats`, `bin/qa/stats.go`) for quantiles/JSONL. Links with **gcc/g++**, not Zig. Ladybug stays the graph; web-search cache stays modernc sqlite. Slice small structured docs with **mikefarah/yq**, not kislyuk/jq. [#30](https://git.produktor.io/eSlider/2dph/issues/30). |
| D23 | CLI | **flaggy** (`github.com/integrii/flaggy`, 0 deps). Flags at any position. Wrapper `internal/cli`. Bash complete: `source <(./bin/cli/complete.go bash)`. No cobra, no stdlib `flag` in Go tools. Search does not intercept the word `completion`. [#34](https://git.produktor.io/eSlider/2dph/issues/34). |
| D22 | analytics | **duckdb-go** in-process (`pkg/duckstats`, `bin/qa/stats.go`) for quantiles/JSONL. Links with **gcc/g++**, not Zig. Ladybug stays the graph; web-search cache stays modernc sqlite. Slice small structured docs with **mikefarah/yq**, not kislyuk/jq. [#30](https://git.produktor.io/eSlider/2dph/issues/30). |
| D23 | CLI | **flaggy** (`github.com/integrii/flaggy`, 0 deps). Flags at any position. Wrapper `pkg/cli`. Bash complete: `source <(./bin/cli/complete.go bash)`. No cobra, no stdlib `flag` in Go tools. Search does not intercept the word `completion`. [#34](https://git.produktor.io/eSlider/2dph/issues/34). |
| D24 | fact intervals | Leaf `valid_from` / `valid_to` (YYYY-MM-DD, inclusive; empty = open/legacy). Search `--as-of` / MCP `as_of` keeps facts active that day. Not D16 `temporal_freshness` (source stale vs HEAD). Empty interval = always visible. [#36](https://git.produktor.io/eSlider/2dph/issues/36). |
| D25 | deploy data path | Brain serves from host `var/`, not named volumes. Compose binds `./var:/data/var` (kb.lbug at `/data/var/kb.lbug`), HF model from `var/hf`, Ladybug FTS/VECTOR extensions mounted read-only into `$HOME/.lbdb/extension`. `brain` uses `network_mode: host` so `127.0.0.1:8630` works with any image (KB_HOST-independent). Named volumes `kb-model`/`kb-var` dropped — live data is host `var/` (gitignored). |

Expand All @@ -65,30 +65,34 @@ detective method: **a fact needs ≥2 independent sources or it is
docs/ published docs (this conversation → docs/ as md)
skills/ in-project skills (web-search, postgres, brain, picoclaw, diataxis-docs)
bin/
facts/extract.go audit.go crm.go # D14 shebang; Go implementation
kb/index deprecated bash shim → bin/brain/index.go
kb/add deprecated bash shim → bin/brain/add.go
brain/search.go deduction: facts → info → web
brain/index.go rebuild FTS + HNSW (incl. --with-mail)
brain/add.go incremental leaf write (no rebuild)
brain/get.go stats.go eval.go # Go read (cgo)
brain/watch.go
brain/search.go deduction: facts → info → web
cli/complete.go flaggy bash/zsh/fish complete (D23)
brain/serve.go HTTP API in-process + OpenAPI/MCP (D20); Zig CGO (D21)
cgo/zig zcc zc++ CGO toolchain (zig cc, not gcc)
mail/import.go JSON → markdown (no brain write)
brain/get.go stats.go eval.go watch.go model.go # Go read/ops (cgo)
facts/extract.go audit.go crm.go # 2-source pairing, confidence, CRM proof
mail/sync.go import.go ocr.go # mail ETL (Gmail/OO/M365), PDF OCR
chats/sync.go import.go facts.go apply.go # conversations (no chats index)
contacts/*.go CRM contacts importer
web/search.go SearXNG client (throttled ≠ absence)
git/import.go go-git history (no git binary; conversion only)
markdown/import.go H2 leaf split (Go)
postgres/query.go read-only YAML (wraps bin/db/psql-yq)
git/import.go go-git history (no git binary; conversion only)
web/search.go SearXNG client (throttled ≠ absence)
reasoner/bakeoff.go CPU tool-call bake-off (D18; OpenAI tools)
chats/sync.go import.go facts.go apply.go
(libs in internal/chats; no chats index)
mail/ocr.go tesseract eng+deu (pdftoppm scans)
brain/extract brain/audit brain/deduce (thinking wrapper)
qa/stats.go DuckDB quantiles / JSONL (D22; not the test taxonomy)
ci/semver.go next semver from conventional commits (Release job)
cli/complete.go flaggy bash/zsh/fish complete (D23)
cgo/zig zcc zc++ CGO toolchain (zig cc, not gcc)
stack/start start-assistant start-mail-sync stop status
db/psql-yq (vendored)
ssh-tunnel onlyoffice pg tunnel 5433
db/psql-yq (vendored)
ssh-tunnel onlyoffice pg tunnel 5433
test/
system/ offline-gated system tests (CI default)
stress/ live-brain load generator
integration/ opt-in live-dependency tests
README.md how to run each tier
etc/
searxng/ picoclaw/ operator config (FHS; moved from deploy/)
var/kb.lbug single embedded store (gitignored)
```

Expand Down Expand Up @@ -130,7 +134,7 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
`eng+deu` (`bin/mail/ocr.go`, `internal/ocr`). No gocv, no gosseract CGO
(D21 Zig owns Ladybug CGO). Optional `OCR_ENGINE=paddle` / compose profile
`ocr-paddle`. Docling left the default path. [#6](https://git.produktor.io/eSlider/2dph/issues/6).
- OQ3: **in** — duckdb-go (`internal/duckstats`, `bin/qa/stats.go`) for
- OQ3: **in** — duckdb-go (`pkg/duckstats`, `bin/qa/stats.go`) for
quantiles / JSONL count. Not a second graph. [#30](https://git.produktor.io/eSlider/2dph/issues/30).
- OQ4: YAML-first storage for leafs — deferred: JSON is ~10x faster to
serialize and unambiguous; YAML only where humans edit files.
Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,49 @@ vector** + **BM25 full-text** indexes. Search is *deduction*: confirmed facts
first, supporting info second, `web-search` as the independent second source
when the local graph cannot confirm.

## What's in 2dph today

- **Single embedded store** — one file `var/kb.lbug` (LadybugDB).
- **Native property graph** + Cypher.
- **Native HNSW** (vectors, 256-dim model2vec) + **BM25 FTS**.
- **Hybrid search** — `facts → info → web`.
- **Graph-hop** (`--hop N`: File → Commit → Person).
- **ACID transactions** — facts + info in one transaction.
- **Incremental write** + bulk rebuild.
- **DuckDB** as auxiliary (D22 / OQ3): quantiles, JSONL stats via duckdb-go
in-process — a helper tool, **not** the primary store.

Run it: [docs/runbook.md](docs/runbook.md). Design: [docs/design.md](docs/design.md).
Docs index: [docs/README.md](docs/README.md).

## Tool layout (D14)

Every command lives at `bin/{subject}/{method}.go` — one method per file, shared
logic in `internal/`. The filename *is* the invocation, and the subject is the
domain area it acts on:

| Subject | Method | Does |
|---------|--------|------|
| `bin/brain` | `search.go` | deduction search (facts → info → web) |
| `bin/brain` | `index.go` / `add.go` | bulk rebuild / incremental write |
| `bin/brain` | `serve.go` | HTTP API + OpenAPI/MCP |
| `bin/facts` | `extract.go` / `audit.go` / `crm.go` | 2-source pairing, confidence, CRM proof |
| `bin/mail` | `sync.go` / `import.go` / `ocr.go` | mail ETL (Gmail/OO/M365) |
| `bin/web` | `search.go` | SearXNG second source |
| `bin/git` | `import.go` | commit history leafs |
| `bin/chats` | `sync.go` / `import.go` / `apply.go` | conversations |
| `bin/stack` | `start` / `status` / `stop` | compose dispatcher |

Go methods are executable (`go run` shebang); a few are thin bash launchers
(`bin/chat`, `bin/db/psql-yq`). Shell completions for all tools (D23) come from
`bin/cli/complete.go` — see the runbook. Keep it one-command-one-file so the
surface stays deductive: you read the path, you know the tool.

**`bin/cgo`** is the CGO toolchain, **not** CI/CD: `zig` (the pinned Zig
compiler), `zcc` / `zc++` (wrappers). Ladybug and tokenizer C libraries are
compiled with `zig cc` (D21), so brain read/write Go binaries link CGO without
a system gcc. CI/CD lives separately in `.github/workflows/ci.yml`.

## Architecture

```mermaid
Expand Down Expand Up @@ -95,7 +135,7 @@ bin/brain/stats.go # index health
bin/brain/eval.go # recall@5 gate
```

`--hop N` walks File/Commit/Person from each hit (max 3). `bin/kb/search` is a deprecated wrapper around `bin/brain/search.go`.
`--hop N` walks File/Commit/Person from each hit (max 3). Search is `bin/brain/search.go`.

Git history is read with [go-git](https://github.com/go-git/go-git) (no git binary):

Expand Down
2 changes: 1 addition & 1 deletion bin/brain/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"os"
"path/filepath"

cliparse "github.com/eSlider/2dph/internal/cli"
cliparse "github.com/eSlider/2dph/pkg/cli"
"github.com/eSlider/2dph/internal/brain"
)

Expand Down
97 changes: 97 additions & 0 deletions bin/brain/import-contact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//usr/bin/env bash -c 'exec "${0%/*}/../cgo/zig" go run -tags=system_ladybug "$0" "$@"' "$0" "$@"; exit
//go:build cgo && system_ladybug
// bin/brain/import-contact.go - read address-book files and upsert each contact
// as an info-root leaf in the 2dph brain.
//
// ./bin/brain/import-contact.go --sources a.csv --db var/kb.lbug
// ./bin/brain/import-contact.go --sources dir/ --dry-run
package main

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/eSlider/2dph/internal/brain"
"github.com/eSlider/2dph/pkg/cli"
"github.com/eSlider/2dph/pkg/contact"
)

func main() {
os.Exit(run(os.Args[1:]))
}

func run(args []string) int {
var (
sources []string
dbPath string
dryRun bool
)
p := cli.New("brain-import-contact")
p.Description = "upsert address-book contacts into the 2dph brain (info-root leafs)"
p.StringSlice(&sources, "s", "sources", "comma-separated files or dirs to read")
p.String(&dbPath, "", "db", "path to kb.lbug (default var/kb.lbug)")
p.Bool(&dryRun, "", "dry-run", "print parsed counts only, write nothing")
if err := cli.Parse(p, args); err != nil {
return cli.Fail(err)
}
if len(sources) == 0 {
fmt.Fprintln(os.Stderr, "brain-import-contact: --sources is required")
return 2
}
var srcs []string
for _, s := range sources {
for _, part := range strings.Split(s, ",") {
if p := strings.TrimSpace(part); p != "" {
srcs = append(srcs, p)
}
}
}
cs, err := contact.Load(srcs)
if err != nil {
fmt.Fprintf(os.Stderr, "brain-import-contact: %v\n", err)
return 1
}
cs = contact.Dedupe(cs)
contact.PrintCounts(cs)
if dryRun {
return 0
}
if dbPath == "" {
dbPath = filepath.Join(brain.RepoRoot(), "var", "kb.lbug")
}
db, conn, err := brain.OpenWritable(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "brain-import-contact: open brain: %v\n", err)
return 1
}
defer db.Close()
defer conn.Close()
if err := brain.InitSchema(conn); err != nil {
fmt.Fprintf(os.Stderr, "brain-import-contact: schema: %v\n", err)
return 1
}
leafs := make([]brain.LeafInput, 0, len(cs))
for _, ct := range cs {
leafs = append(leafs, brain.LeafInput{
Text: ct.Markdown(),
Root: "info",
Type: "contact",
How: "brain/import-contact",
Source: ct.Source,
Loc: ct.Source,
})
}
ids, err := brain.AddLeafs(conn, leafs)
if err != nil {
fmt.Fprintf(os.Stderr, "brain-import-contact: write: %v\n", err)
return 1
}
if err := brain.EnsureIndexes(conn); err != nil {
fmt.Fprintf(os.Stderr, "brain-import-contact: indexes: %v\n", err)
return 1
}
fmt.Fprintf(os.Stderr, "brain: wrote %d leaf(s) to %s\n", len(ids), dbPath)
return 0
}
2 changes: 1 addition & 1 deletion bin/brain/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"time"

"github.com/eSlider/2dph/internal/brain"
cliparse "github.com/eSlider/2dph/internal/cli"
cliparse "github.com/eSlider/2dph/pkg/cli"
)

func main() {
Expand Down
1 change: 0 additions & 1 deletion bin/brain/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
// ./bin/brain/search.go --list-model
//
// Shebang routes through bin/cgo/zig (Zig cc + liblbug), not gcc.
// bin/kb/search builds a cached binary the same way.
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
package main

Expand Down
2 changes: 1 addition & 1 deletion bin/brain/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"os"

"github.com/eSlider/2dph/internal/brain"
"github.com/eSlider/2dph/internal/httpapi"
"github.com/eSlider/2dph/pkg/httpapi"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion bin/brain/serve_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package main
import (
"os"

"github.com/eSlider/2dph/internal/httpapi"
"github.com/eSlider/2dph/pkg/httpapi"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion bin/cli/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
mailsync "github.com/eSlider/2dph/bin/mail/sync"
"github.com/eSlider/2dph/internal/brain/rank"
"github.com/eSlider/2dph/internal/chats"
"github.com/eSlider/2dph/internal/cli"
"github.com/eSlider/2dph/pkg/cli"
"github.com/eSlider/2dph/internal/gitlog"
"github.com/eSlider/2dph/internal/mdleaves"
"github.com/eSlider/2dph/internal/ocr"
Expand Down
Loading
Loading