Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .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 All @@ -48,7 +47,7 @@ jobs:
- name: Go tests (root module, no duckdb CGO; Gitea runner libstdc++ is too old for duckdb static lib)
if: github.server_url != 'https://github.com'
run: |
PKGS="$(go list ./... | grep -v /internal/duckstats)"
PKGS="$(go list ./... | grep -v /pkg/duckdb)"
go vet $PKGS
go test $PKGS -count=1

Expand Down
46 changes: 28 additions & 18 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 import-git.go search.go serve.go index.go add.go get.go stats.go eval.go watch.go
bin/contact/ 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/mail/ sync.go import.go convert-mbox.go ocr.go (lib: internal/mailsync)
bin/markdown/ split-leaf.go (H2 leaf split; lib: internal/markdown)
bin/jsonl/ stats.go (DuckDB quantiles / JSONL count; gcc CGO, not Zig)
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)
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/reasoner/ bench.go (D18 CPU OpenAI tool-call bench)
bin/facts/ extract.go audit.go audit-db.go prove-crm.go
bin/shell/ complete.go (flaggy completions dump, D23)
pkg/ public reusable Go (cli flaggy D23; repo; contact; duckdb; httpapi) — no 2dph deps
internal/ private 2dph Go (brain, chats, facts D16, gitlog, websearch, reasoner, mailsync, mailconv, corpuswatch, markdown)
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 @@ -94,12 +105,11 @@ 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/facts/prove-crm.go [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
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
source <(./bin/cli/complete.go bash) # flaggy completions (D23)
source <(./bin/shell/complete.go bash) # flaggy completions (D23)
eval "$(bin/cgo/zig env)" # optional; Ladybug shebangs call bin/cgo/zig
bin/brain/index.go --rebuild [--with-mail] [--with-facts] [--with-chats]
bin/brain/add.go --text T --root facts --source "a.md x b.md" # incremental write
Expand All @@ -112,12 +122,12 @@ bin/stack/start # brain HTTP/MCP (reuse healthy
bin/stack/start-assistant # + reasoner + PicoClaw agent
bin/stack/status # YAML health
bin/stack/stop # compose stop; volumes kept
bin/markdown/import.go [dir] # H2 leafs → YAML (Go)
bin/git/import.go [REPO] [--json] [--limit N] # go-git history → commit leafs
bin/markdown/split-leaf.go [dir] # H2 leafs → YAML (Go)
bin/brain/import-git.go [REPO] [--json] [--limit N] # go-git history → commit leafs
bin/web/search.go "query" [--json] # SearXNG; throttled ≠ absence
bin/reasoner/bakeoff.go [--model ID] [--json] # D18 CPU tool-call bake-off
bin/reasoner/bench.go [--model ID] [--json] # D18 CPU tool-call bake-off
bin/postgres/query.go --profile onlyoffice -c 'SELECT 1'
bin/qa/stats.go # D22 DuckDB quantiles / JSONL (gcc CGO)
bin/jsonl/stats.go # D22 DuckDB quantiles / JSONL (gcc CGO)
bin/mail/ocr.go <image|pdf> # tesseract eng+deu (scans)
bin/md/tables # what the graph holds → YAML
bin/brain/deduce "question" # thinking wrapper
Expand All @@ -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/duckdb`, `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
52 changes: 28 additions & 24 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ detective method: **a fact needs ≥2 independent sources or it is
| D15 | repo | Gitea [`eSlider/2dph`](https://git.produktor.io/eSlider/2dph) is origin + [issues](https://git.produktor.io/eSlider/2dph/issues). GitHub `eSlider/2dph` is the public clone (PRs + Actions CI). No direct `main` pushes. TDD → PR → CI green → merge. |
| D16 | contradictions | ≥2 yes vs ≥2 no → hypothesis → `(not confirmed)` until a rule fires. Order: **temporal_freshness** (fresh ≥2 vs stale minority), then **authority_pairing** (runtime/config A×B beats narrative C). Store as `a x b vs c x d` on hypothesis leafs. `bin/facts/audit contradict`. [#29](https://git.produktor.io/eSlider/2dph/issues/29). |
| 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`). |
| 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/bench.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/brain/import-git.go` (lib `internal/gitlog`). No subprocess of the git binary. Reads history and upserts commit leafs into the brain directly (`--dry-run` to preview). |
| 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/duckdb`, `bin/jsonl/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/shell/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 convert-mbox.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)
reasoner/bench.go CPU tool-call bench (D18; OpenAI tools)
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/duckdb`, `bin/jsonl/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
48 changes: 44 additions & 4 deletions 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/shell/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 All @@ -32,7 +72,7 @@ graph TB
EX["bin/facts/extract.go<br/>2-source pairing"]
AU["bin/facts/audit.go<br/>confidence + staleness"]
IDX["bin/brain/index.go<br/>chunk + embed"]
MD["bin/markdown/import.go<br/>H2 leaf split"]
MD["bin/markdown/split-leaf.go<br/>H2 leaf split"]
SR["bin/brain/search.go<br/>deduction"]
end

Expand Down Expand Up @@ -95,13 +135,13 @@ 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):

```bash
bin/git/import.go --json --limit 100 # commit leafs for this repo
bin/git/import.go --root "$PROJECTS_ROOT" --json # one pass per .git under root
bin/brain/import-git.go --json --limit 100 # commit leafs for this repo
bin/brain/import-git.go --root "$PROJECTS_ROOT" --json # one pass per .git under root
```

Conversion only. Graph write (`File-[:HAS_VERSION]->Commit-[:AUTHORED]->Person`) stays with `bin/brain/index.go`.
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
Loading
Loading