Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
515088f4e1 | ||
|
|
368a757612 | ||
|
|
a8675ac33b | ||
|
|
de632ba6cc | ||
|
|
66c87842e2 | ||
|
|
20b78a9a20 | ||
|
|
5d4b3427a4 | ||
|
|
1c7db6d499 | ||
|
|
0786ddcb06 | ||
|
|
eeb5b79cf2 | ||
|
|
5990feb1f6 | ||
|
|
d27a738fee | ||
|
|
669e184cf6 | ||
|
|
ebc3f948c1 | ||
|
|
fe6a02024c | ||
|
|
4a065d9838 | ||
|
|
e3c6ef5684 | ||
|
|
98c14e23f1 | ||
|
|
ff1716de40 | ||
|
|
fec5325c7a | ||
|
|
27d9521e7f | ||
|
|
a7cb8d4c76 | ||
|
|
53cd00284d | ||
|
|
b73b4d4f97 | ||
|
|
1d1f6a90ff | ||
|
|
f220bcd95a | ||
|
|
678a1d1dba | ||
|
|
8781c0c3eb | ||
|
|
d6b17e8819 |
@@ -77,14 +77,12 @@ bin/brain/index.go --rebuild # rebuil
|
|||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/facts/audit.go ["self"|"facts"|"info"|"stale"] # 2-source + staleness gate
|
bin/facts/audit ["self"|"facts"|"info"|"stale"] # 2-source + staleness gate
|
||||||
bin/facts/crm.go [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
|
bin/facts/crm [--dry-run] # proof person↔company/company↔project (ooCRM × corpus SoT)
|
||||||
bin/kb/search "query" [--repo X] # deprecated wrapper → bin/brain/search.go
|
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" [--root facts|info] # deduction search → YAML
|
||||||
bin/brain/search.go "query" --no-web # local graph only
|
bin/brain/search.go "query" --no-web # local graph only
|
||||||
bin/brain/get.go <id> [--body] [--json] # Go read; Python bin/kb/get CI fallback
|
bin/brain/get.go <id> [--body]
|
||||||
bin/brain/stats.go [--json]
|
|
||||||
bin/brain/eval.go [--json] # recall@5; questions in internal/brain/rank
|
|
||||||
bin/markdown/import.go [dir] # mistune leaves → YAML
|
bin/markdown/import.go [dir] # mistune leaves → YAML
|
||||||
bin/git/import.go [REPO] [--json] [--limit N] # go-git history → commit leafs
|
bin/git/import.go [REPO] [--json] [--limit N] # go-git history → commit leafs
|
||||||
bin/web/search.go "query" [--json] # SearXNG; throttled ≠ absence
|
bin/web/search.go "query" [--json] # SearXNG; throttled ≠ absence
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ detective method: **a fact needs ≥2 independent sources or it is
|
|||||||
| D3 | web search | Go client `bin/web/search.go` (`internal/websearch`). SearXNG URL is config (`BRAIN_SEARCH_URL`). Optional Compose profile `searxng` (sanitized settings). Do not run a second copy on a host that already has one. Empty/`throttled` ≠ “nothing exists”. |
|
| D3 | web search | Go client `bin/web/search.go` (`internal/websearch`). SearXNG URL is config (`BRAIN_SEARCH_URL`). Optional Compose profile `searxng` (sanitized settings). Do not run a second copy on a host that already has one. Empty/`throttled` ≠ “nothing exists”. |
|
||||||
| D4 | embeddings | **model2vec** `minishlab/potion-multilingual-128M` instead of embeddinggemma. |
|
| D4 | embeddings | **model2vec** `minishlab/potion-multilingual-128M` instead of embeddinggemma. |
|
||||||
| D5 | parser | **mistune** for MD → leaf extraction (duckdb-md documented as future optional SQL/export layer, not v1). |
|
| D5 | parser | **mistune** for MD → leaf extraction (duckdb-md documented as future optional SQL/export layer, not v1). |
|
||||||
| D6 | graph engine | **LadybugDB**. Go is the service (`bin/brain/search.go`, `bin/brain/serve.go` in-process, `internal/brain`). Read path (`get.go` / `stats.go` / `eval.go`) is Go + cgo. Python `bin/kb/{get,stats,eval}` is the CI fallback (GitHub runners have no ladybug cgo). Index/write stays Python until the Go write path is safe. |
|
| D6 | graph engine | **LadybugDB**. Go is the service (`bin/brain/search.go`, `bin/brain/serve.go` in-process, `internal/brain`); Python remains for index/write until the Go write path is safe. |
|
||||||
| D7 | db access | `db-yaml`/`psql-yq`-style, read-only, YAML out. OnlyOffice Postgres via SSH tunnel (`127.0.0.1:5433`). |
|
| D7 | db access | `db-yaml`/`psql-yq`-style, read-only, YAML out. OnlyOffice Postgres via SSH tunnel (`127.0.0.1:5433`). |
|
||||||
| D8 | evidence | detective method: ≥2 independent sources or `(not confirmed)`. Auto-pair docker ps × compose × ssh-config × docs. |
|
| D8 | evidence | detective method: ≥2 independent sources or `(not confirmed)`. Auto-pair docker ps × compose × ssh-config × docs. |
|
||||||
| D9 | facts/goal model | Who / What / How / Where / When + evidence + confidence on every edge. |
|
| D9 | facts/goal model | Who / What / How / Where / When + evidence + confidence on every edge. |
|
||||||
@@ -52,11 +52,11 @@ detective method: **a fact needs ≥2 independent sources or it is
|
|||||||
docs/ published docs (this conversation → docs/ as md)
|
docs/ published docs (this conversation → docs/ as md)
|
||||||
skills/ in-project skills (web-search, db-yaml, brain, diataxis-docs)
|
skills/ in-project skills (web-search, db-yaml, brain, diataxis-docs)
|
||||||
bin/
|
bin/
|
||||||
facts/extract.go audit.go crm.go # D14 shebang; Python implementation
|
facts/extract auto-pair 2 sources → lexicon yaml + graph
|
||||||
|
facts/audit ["self"|"facts"|"info"|"stale"] 2-source + staleness gate
|
||||||
kb/index Python write path (called by bin/brain/index.go)
|
kb/index Python write path (called by bin/brain/index.go)
|
||||||
brain/index.go rebuild FTS + HNSW (incl. --with-mail)
|
brain/index.go rebuild FTS + HNSW (incl. --with-mail)
|
||||||
brain/get.go stats.go eval.go # Go read (cgo); Python bin/kb/* CI fallback
|
brain/get.go stats.go eval.go watch.go
|
||||||
brain/watch.go
|
|
||||||
brain/search.go deduction: facts → info → web-search
|
brain/search.go deduction: facts → info → web-search
|
||||||
brain/serve.go HTTP API in-process (internal/httpapi + internal/brain)
|
brain/serve.go HTTP API in-process (internal/httpapi + internal/brain)
|
||||||
mail/import.go JSON → markdown (no brain write)
|
mail/import.go JSON → markdown (no brain write)
|
||||||
@@ -132,10 +132,8 @@ Common props on every node/edge: `root`, `confidence`, `evidence[]`, `how`,
|
|||||||
1. go vet + go test ./... (root module; packages without ladybug cgo)
|
1. go vet + go test ./... (root module; packages without ladybug cgo)
|
||||||
2. `go test ./internal/brain/rank` (cgo-free ranking + flag parser)
|
2. `go test ./internal/brain/rank` (cgo-free ranking + flag parser)
|
||||||
3. python -m unittest discover -s bin/tools (includes published-docs SoT)
|
3. python -m unittest discover -s bin/tools (includes published-docs SoT)
|
||||||
4. `bin/facts/audit self` (lexicon internal consistency; `bin/facts/audit.go` is the D14 wrapper)
|
4. bin/facts/audit self (lexicon internal consistency)
|
||||||
5. `bin/kb/eval` (recall@5 ≥ 0.95). Local SoT is `bin/brain/eval.go`; CI uses
|
5. bin/brain/eval.go (recall@5 ≥ 0.95, gates index regressions)
|
||||||
the Python twin until the runner has ladybug cgo. Questions live in
|
|
||||||
`internal/brain/rank`.
|
|
||||||
6. md-docs build/lint if docs tooling arrives.
|
6. md-docs build/lint if docs tooling arrives.
|
||||||
|
|
||||||
Feedback loop: every commit → PR → CI → green/gate → merge. Same discipline as
|
Feedback loop: every commit → PR → CI → green/gate → merge. Same discipline as
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ graph TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph dph["2dph tools"]
|
subgraph dph["2dph tools"]
|
||||||
EX["bin/facts/extract.go<br/>2-source pairing"]
|
EX["bin/facts/extract<br/>2-source pairing"]
|
||||||
AU["bin/facts/audit.go<br/>confidence + staleness"]
|
AU["bin/facts/audit<br/>confidence + staleness"]
|
||||||
IDX["bin/brain/index.go<br/>chunk + embed"]
|
IDX["bin/brain/index.go<br/>chunk + embed"]
|
||||||
MD["bin/markdown/import.go<br/>mistune leaves"]
|
MD["bin/markdown/import.go<br/>mistune leaves"]
|
||||||
SR["bin/brain/search.go<br/>deduction"]
|
SR["bin/brain/search.go<br/>deduction"]
|
||||||
@@ -126,8 +126,7 @@ bin/brain/search.go "invoice from last week" # same s
|
|||||||
|
|
||||||
- **LadybugDB** — single `var/kb.lbug`, Cypher property graph, HNSW + BM25
|
- **LadybugDB** — single `var/kb.lbug`, Cypher property graph, HNSW + BM25
|
||||||
in one engine, embedded (no server), ACID, read-only-safe for concurrent
|
in one engine, embedded (no server), ACID, read-only-safe for concurrent
|
||||||
readers. Read tools (`get` / `stats` / `eval`) are Go + cgo; Python
|
readers. **Never `DROP INDEX` FTS/VECTOR** on Ladybug 0.19: DROP leaves
|
||||||
`bin/kb/{get,stats,eval}` is the CI fallback. **Never `DROP INDEX` FTS/VECTOR** on Ladybug 0.19: DROP leaves
|
|
||||||
ghost catalog tables (`_0_Leaf_vec_UPPER`) so recreate fails while
|
ghost catalog tables (`_0_Leaf_vec_UPPER`) so recreate fails while
|
||||||
`SHOW_INDEXES` omits HNSW. Fresh indexes = delete `var/kb.lbug` +
|
`SHOW_INDEXES` omits HNSW. Fresh indexes = delete `var/kb.lbug` +
|
||||||
`bin/brain/index.go --rebuild`. Use `ensure_indexes()` after upserts.
|
`bin/brain/index.go --rebuild`. Use `ensure_indexes()` after upserts.
|
||||||
@@ -148,7 +147,7 @@ machines. Tests gate every commit. HTTP: `bin/brain/serve.go` calls
|
|||||||
```bash
|
```bash
|
||||||
uv venv .venv # Python 3.12, uv-managed
|
uv venv .venv # Python 3.12, uv-managed
|
||||||
uv pip install -r requirements.lock.txt # pinned toolchain
|
uv pip install -r requirements.lock.txt # pinned toolchain
|
||||||
bin/facts/audit.go self # lexicon consistency gate
|
bin/facts/audit self # lexicon consistency gate
|
||||||
go test ./... && python -m unittest discover -s bin/tools -t .
|
go test ./... && python -m unittest discover -s bin/tools -t .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+4
-6
@@ -1,22 +1,20 @@
|
|||||||
//usr/bin/env go run -tags=system_ladybug,brain_eval "$0" "$@"; exit
|
//usr/bin/env go run -tags=brain_eval "$0" "$@"; exit
|
||||||
//go:build cgo && system_ladybug && brain_eval
|
//go:build brain_eval
|
||||||
//
|
//
|
||||||
// bin/brain/eval.go - recall@5 gate.
|
// bin/brain/eval.go - recall@5 gate.
|
||||||
//
|
//
|
||||||
// ./bin/brain/eval.go
|
// ./bin/brain/eval.go
|
||||||
// ./bin/brain/eval.go --json
|
// ./bin/brain/eval.go --json
|
||||||
//
|
//
|
||||||
// Needs CGO + libladybug. Python bin/kb/eval is the CI fallback (no cgo).
|
|
||||||
// Control questions live in internal/brain/rank (cgo-free).
|
|
||||||
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/brain"
|
"github.com/eSlider/2dph/internal/cmdbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(brain.MainEval(os.Args[1:]))
|
os.Exit(cmdbin.ExecFile("bin/kb/eval", os.Args[1:]))
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-6
@@ -1,22 +1,20 @@
|
|||||||
//usr/bin/env go run -tags=system_ladybug,brain_get "$0" "$@"; exit
|
//usr/bin/env go run -tags=brain_get "$0" "$@"; exit
|
||||||
//go:build cgo && system_ladybug && brain_get
|
//go:build brain_get
|
||||||
//
|
//
|
||||||
// bin/brain/get.go - read one leaf by id.
|
// bin/brain/get.go - read one leaf by id.
|
||||||
//
|
//
|
||||||
// ./bin/brain/get.go <id>
|
// ./bin/brain/get.go <id>
|
||||||
// ./bin/brain/get.go <id> --body
|
// ./bin/brain/get.go <id> --body
|
||||||
// ./bin/brain/get.go <id> --json
|
|
||||||
//
|
//
|
||||||
// Needs CGO + libladybug. Python bin/kb/get is the CI fallback (no cgo).
|
|
||||||
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/brain"
|
"github.com/eSlider/2dph/internal/cmdbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(brain.MainGet(os.Args[1:]))
|
os.Exit(cmdbin.ExecFile("bin/kb/get", os.Args[1:]))
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -1,21 +1,20 @@
|
|||||||
//usr/bin/env go run -tags=system_ladybug,brain_stats "$0" "$@"; exit
|
//usr/bin/env go run -tags=brain_stats "$0" "$@"; exit
|
||||||
//go:build cgo && system_ladybug && brain_stats
|
//go:build brain_stats
|
||||||
//
|
//
|
||||||
// bin/brain/stats.go - index health.
|
// bin/brain/stats.go - index health.
|
||||||
//
|
//
|
||||||
// ./bin/brain/stats.go
|
// ./bin/brain/stats.go
|
||||||
// ./bin/brain/stats.go --json
|
// ./bin/brain/stats.go --json
|
||||||
//
|
//
|
||||||
// Needs CGO + libladybug. Python bin/kb/stats is the CI fallback (no cgo).
|
|
||||||
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/brain"
|
"github.com/eSlider/2dph/internal/cmdbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(brain.MainStats(os.Args[1:]))
|
os.Exit(cmdbin.ExecFile("bin/kb/stats", os.Args[1:]))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
//usr/bin/env go run -tags=facts_audit "$0" "$@"; exit
|
|
||||||
//go:build facts_audit
|
|
||||||
//
|
|
||||||
// bin/facts/audit.go - 2-source + lexicon checks.
|
|
||||||
//
|
|
||||||
// ./bin/facts/audit.go self
|
|
||||||
// ./bin/facts/audit.go db
|
|
||||||
//
|
|
||||||
// Python bin/facts/audit is the implementation (CI runs it directly).
|
|
||||||
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/cmdbin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
os.Exit(cmdbin.ExecFile("bin/facts/audit", os.Args[1:]))
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
//usr/bin/env go run -tags=facts_crm "$0" "$@"; exit
|
|
||||||
//go:build facts_crm
|
|
||||||
//
|
|
||||||
// bin/facts/crm.go - prove person↔company / company↔project (ooCRM × corpus).
|
|
||||||
//
|
|
||||||
// ./bin/facts/crm.go [--dry-run] [--mismatches]
|
|
||||||
//
|
|
||||||
// Python bin/facts/crm is the implementation. Graph write stays Python.
|
|
||||||
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/cmdbin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
os.Exit(cmdbin.ExecFile("bin/facts/crm", os.Args[1:]))
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
//usr/bin/env go run -tags=facts_extract "$0" "$@"; exit
|
|
||||||
//go:build facts_extract
|
|
||||||
//
|
|
||||||
// bin/facts/extract.go - acquire confirmed facts (2-source each).
|
|
||||||
//
|
|
||||||
// ./bin/facts/extract.go [--json] [--dry-run]
|
|
||||||
//
|
|
||||||
// Python bin/facts/extract is the implementation. Graph write stays Python.
|
|
||||||
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/cmdbin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
os.Exit(cmdbin.ExecFile("bin/facts/extract", os.Args[1:]))
|
|
||||||
}
|
|
||||||
@@ -77,44 +77,6 @@ class BinLayoutTest(unittest.TestCase):
|
|||||||
for method in ("index.go", "get.go", "stats.go", "eval.go", "watch.go"):
|
for method in ("index.go", "get.go", "stats.go", "eval.go", "watch.go"):
|
||||||
self._assert_shebang(f"bin/brain/{method}")
|
self._assert_shebang(f"bin/brain/{method}")
|
||||||
|
|
||||||
def test_brain_get_stats_eval_are_not_python_exec(self) -> None:
|
|
||||||
for method in ("get.go", "stats.go", "eval.go"):
|
|
||||||
text = (ROOT / "bin" / "brain" / method).read_text()
|
|
||||||
self.assertNotIn(
|
|
||||||
"ExecFile",
|
|
||||||
text,
|
|
||||||
f"bin/brain/{method} must call internal/brain, not ExecFile Python",
|
|
||||||
)
|
|
||||||
self.assertNotIn(
|
|
||||||
"cmdbin",
|
|
||||||
text,
|
|
||||||
f"bin/brain/{method} must not import internal/cmdbin",
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"system_ladybug",
|
|
||||||
text.splitlines()[0],
|
|
||||||
f"bin/brain/{method} shebang must pass -tags=system_ladybug",
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"github.com/eSlider/2dph/internal/brain",
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_eval_control_questions_live_in_rank(self) -> None:
|
|
||||||
rank = (ROOT / "internal" / "brain" / "rank" / "evalq.go").read_text()
|
|
||||||
py = (ROOT / "bin" / "kb" / "eval").read_text()
|
|
||||||
for frag in ("BM25", "DevOps", "LadybugDB"):
|
|
||||||
self.assertIn(frag, rank)
|
|
||||||
self.assertIn(frag, py)
|
|
||||||
self.assertIn("0.95", rank)
|
|
||||||
|
|
||||||
def test_facts_methods_are_shebangs(self) -> None:
|
|
||||||
for method in ("audit.go", "extract.go", "crm.go"):
|
|
||||||
self._assert_shebang(f"bin/facts/{method}")
|
|
||||||
text = (ROOT / "bin" / "facts" / method).read_text()
|
|
||||||
self.assertIn("cmdbin.ExecFile", text)
|
|
||||||
self.assertIn(f"bin/facts/{method.removesuffix('.go')}", text)
|
|
||||||
|
|
||||||
def test_mail_import_is_shebang_not_brain_write(self) -> None:
|
def test_mail_import_is_shebang_not_brain_write(self) -> None:
|
||||||
self._assert_shebang("bin/mail/import.go")
|
self._assert_shebang("bin/mail/import.go")
|
||||||
index_mail = (ROOT / "bin" / "mail" / "index_mail").read_text()
|
index_mail = (ROOT / "bin" / "mail" / "index_mail").read_text()
|
||||||
|
|||||||
@@ -59,14 +59,6 @@ class PublishedDocsTest(unittest.TestCase):
|
|||||||
self.assertNotIn("password", settings.lower())
|
self.assertNotIn("password", settings.lower())
|
||||||
self.assertIn("json", settings)
|
self.assertIn("json", settings)
|
||||||
|
|
||||||
def test_readme_read_path_is_go(self) -> None:
|
|
||||||
plan = (ROOT / "PLAN.md").read_text()
|
|
||||||
self.assertIn("get.go", plan)
|
|
||||||
self.assertIn("CI fallback", plan)
|
|
||||||
design = (ROOT / "docs" / "design.md").read_text()
|
|
||||||
self.assertIn("internal/brain/rank", design)
|
|
||||||
self.assertIn("They do not exec Python", design)
|
|
||||||
|
|
||||||
def test_readme_search_escalates_web(self) -> None:
|
def test_readme_search_escalates_web(self) -> None:
|
||||||
text = (ROOT / "README.md").read_text()
|
text = (ROOT / "README.md").read_text()
|
||||||
self.assertIn("--no-web", text)
|
self.assertIn("--no-web", text)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# CRM association proof (oo CLI ↔ corpus)
|
||||||
|
|
||||||
|
Proven with `oo` (eslider/go-onlyoffice) against the OnlyOffice portal
|
||||||
|
(`office.produktor.io`). Portal CRM is the SSOT for company ↔ person ↔
|
||||||
|
project associations; the corpus SoT (`eslider/cv/projects/knowledge-mesh-seed.yaml`)
|
||||||
|
is the second, independent source. Facts that can be backed by both are
|
||||||
|
written to the brain under `root=facts` by `bin/facts/crm`.
|
||||||
|
|
||||||
|
## What was verified
|
||||||
|
|
||||||
|
- Logical counts (portal MySQL): 1300 contacts = 897 persons + 404 companies,
|
||||||
|
198 projects, 998 deals, 939 project↔contact links.
|
||||||
|
- Every client company linked to a project has ≥1 person underneath.
|
||||||
|
- Every person `company_id` resolves to an existing company.
|
||||||
|
- Corpus org list (9) maps 1:1 onto CRM companies:
|
||||||
|
ProProdukt SL / produktor.io, Dyvenia, Immowelt AG, WhereGroup,
|
||||||
|
Keynote SIGOS, D2S/SYSTEMS, GRID, Pack und Cup, Markets Platform.
|
||||||
|
- 78 person↔company association facts written to the brain
|
||||||
|
(`how=crm-crosscheck`, `type=association`). Recall@5 in `bin/kb/eval` = 1.0.
|
||||||
|
|
||||||
|
## Mistakes found
|
||||||
|
|
||||||
|
| # | Mistake | Fix |
|
||||||
|
|---|---------|-----|
|
||||||
|
| 1 | Duplicate legal entity `GoldenRatio.Exchange` (contact 759) vs `Golden Ratio Exchange` (763); 3 deals (211, 287, 559) were linked to 759 | `oo contacts merge 759 763` — 763 kept, 759 removed, deal links re-pointed to 763 |
|
||||||
|
| 2 | `env/`-wide: OnlyOffice creds file used wrong UX (user `eslider`, password with `$2` suffix) making `oo` auth fail | `.env` fixed to `eslider@gmail.com` + clean password; `.env` stays gitignored |
|
||||||
|
|
||||||
|
## Gates after fix
|
||||||
|
|
||||||
|
- `uv run python -m unittest discover -s bin/tools -t .` → 26 tests OK
|
||||||
|
- `bin/facts/audit self` + `bin/facts/audit db` → ok
|
||||||
|
- `bin/kb/eval` → recall@5 = 1.0
|
||||||
|
- `go test ./...` (bin/server + bin/watch) → ok
|
||||||
+1
-9
@@ -61,12 +61,4 @@ corpus HEAD.
|
|||||||
- C: narrative — READMEs, AGENTS.md, docs
|
- C: narrative — READMEs, AGENTS.md, docs
|
||||||
|
|
||||||
Confirmed = A×B or B×C agreement. Single source = hypothesis + `(not confirmed)`.
|
Confirmed = A×B or B×C agreement. Single source = hypothesis + `(not confirmed)`.
|
||||||
Conflicting pairings (≥2 yes vs ≥2 no) = hypothesis (OQ1 → v2 resolution).
|
Conflicting pairings (≥2 yes vs ≥2 no) = hypothesis (OQ1 → v2 resolution).
|
||||||
|
|
||||||
## Read path
|
|
||||||
|
|
||||||
`bin/brain/get.go`, `stats.go`, and `eval.go` call `internal/brain` with cgo
|
|
||||||
(`system_ladybug`). They do not exec Python. Control questions for recall@5
|
|
||||||
live in `internal/brain/rank` so CI can test the table without libladybug.
|
|
||||||
Python `bin/kb/{get,stats,eval}` remain for GitHub Actions until the runner
|
|
||||||
has ladybug cgo. Index/write is still `bin/kb/index`.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package brain
|
|
||||||
|
|
||||||
const ModelID = "minishlab/potion-multilingual-128M"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package rank
|
|
||||||
|
|
||||||
// Eval control questions (recall@5). Kept here so CI can test the gate
|
|
||||||
// table without ladybug cgo. The runner lives in internal/brain (cgo).
|
|
||||||
const EvalRecallThreshold = 0.95
|
|
||||||
|
|
||||||
type EvalQuestion struct {
|
|
||||||
Query string
|
|
||||||
Fragment string
|
|
||||||
}
|
|
||||||
|
|
||||||
var EvalQuestions = []EvalQuestion{
|
|
||||||
{"hybrid search fts and vector", "BM25"},
|
|
||||||
{"eslider devops engineer", "DevOps"},
|
|
||||||
{"ladybugdb graph engine storage", "LadybugDB"},
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package rank
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestEvalQuestionsAreThreeAndThreshold(t *testing.T) {
|
|
||||||
if EvalRecallThreshold != 0.95 {
|
|
||||||
t.Fatalf("threshold = %v", EvalRecallThreshold)
|
|
||||||
}
|
|
||||||
if len(EvalQuestions) != 3 {
|
|
||||||
t.Fatalf("questions = %d, want 3", len(EvalQuestions))
|
|
||||||
}
|
|
||||||
for _, q := range EvalQuestions {
|
|
||||||
if q.Query == "" || q.Fragment == "" {
|
|
||||||
t.Fatalf("empty control: %+v", q)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
//go:build cgo && system_ladybug
|
|
||||||
|
|
||||||
package brain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/eSlider/2dph/internal/brain/rank"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MainGet(args []string) int {
|
|
||||||
id, body, jsonOut := "", false, false
|
|
||||||
for _, a := range args {
|
|
||||||
switch {
|
|
||||||
case a == "--body":
|
|
||||||
body = true
|
|
||||||
case a == "--json":
|
|
||||||
jsonOut = true
|
|
||||||
case a == "-h" || a == "--help":
|
|
||||||
fmt.Fprintln(os.Stderr, `usage: bin/brain/get.go <id> [--body] [--json]`)
|
|
||||||
return 0
|
|
||||||
case strings.HasPrefix(a, "-"):
|
|
||||||
fmt.Fprintf(os.Stderr, "brain/get: unknown flag %s\n", a)
|
|
||||||
return 2
|
|
||||||
default:
|
|
||||||
id = a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if id == "" {
|
|
||||||
fmt.Fprintln(os.Stderr, "brain/get: id required")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if err := openBrain(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
defer closeBrain()
|
|
||||||
meta, text, err := lookupLeaf(id)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "brain/get: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
out := Dict{
|
|
||||||
{"id", meta["id"]},
|
|
||||||
{"root", meta["root"]},
|
|
||||||
{"confidence", meta["confidence"]},
|
|
||||||
{"source", meta["source"]},
|
|
||||||
{"type", meta["type"]},
|
|
||||||
}
|
|
||||||
if body {
|
|
||||||
out = append(out, KV{"text", text})
|
|
||||||
} else {
|
|
||||||
out = append(out, KV{"snippet", clip(text, 280)})
|
|
||||||
}
|
|
||||||
if jsonOut {
|
|
||||||
m := map[string]any{}
|
|
||||||
for _, kv := range out {
|
|
||||||
m[kv.K] = kv.V
|
|
||||||
}
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
return b2i(enc.Encode(m))
|
|
||||||
}
|
|
||||||
fmt.Print(toYAML(out, 0))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func MainStats(args []string) int {
|
|
||||||
jsonOut := false
|
|
||||||
for _, a := range args {
|
|
||||||
switch a {
|
|
||||||
case "--json":
|
|
||||||
jsonOut = true
|
|
||||||
case "-h", "--help":
|
|
||||||
fmt.Fprintln(os.Stderr, `usage: bin/brain/stats.go [--json]`)
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
if strings.HasPrefix(a, "-") {
|
|
||||||
fmt.Fprintf(os.Stderr, "brain/stats: unknown flag %s\n", a)
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := openBrain(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
defer closeBrain()
|
|
||||||
s, err := leafStats()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "brain/stats: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if jsonOut {
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
return b2i(enc.Encode(s))
|
|
||||||
}
|
|
||||||
by := s["by_root"].(map[string]int)
|
|
||||||
keys := make([]string, 0, len(by))
|
|
||||||
for k := range by {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
byRoot := make(Dict, 0, len(keys))
|
|
||||||
for _, k := range keys {
|
|
||||||
byRoot = append(byRoot, KV{k, by[k]})
|
|
||||||
}
|
|
||||||
out := Dict{
|
|
||||||
{"total", s["total"]},
|
|
||||||
{"by_root", byRoot},
|
|
||||||
{"db", s["db"]},
|
|
||||||
{"model", s["model"]},
|
|
||||||
}
|
|
||||||
fmt.Print(toYAML(out, 0))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func MainEval(args []string) int {
|
|
||||||
jsonOut := false
|
|
||||||
for _, a := range args {
|
|
||||||
switch a {
|
|
||||||
case "--json":
|
|
||||||
jsonOut = true
|
|
||||||
case "-h", "--help":
|
|
||||||
fmt.Fprintln(os.Stderr, `usage: bin/brain/eval.go [--json]`)
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
if strings.HasPrefix(a, "-") {
|
|
||||||
fmt.Fprintf(os.Stderr, "brain/eval: unknown flag %s\n", a)
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := openBrain(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "open brain: %v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
defer closeBrain()
|
|
||||||
recalled := 0
|
|
||||||
details := make([]any, 0, len(rank.EvalQuestions))
|
|
||||||
jsDetails := make([]map[string]any, 0, len(rank.EvalQuestions))
|
|
||||||
for _, q := range rank.EvalQuestions {
|
|
||||||
hits, err := queryFTS(q.Query, 5)
|
|
||||||
ok := false
|
|
||||||
if err == nil {
|
|
||||||
frag := strings.ToLower(q.Fragment)
|
|
||||||
for _, h := range hits {
|
|
||||||
if strings.Contains(strings.ToLower(h.Text), frag) {
|
|
||||||
ok = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
recalled++
|
|
||||||
}
|
|
||||||
details = append(details, Dict{
|
|
||||||
{"q", q.Query},
|
|
||||||
{"fragment", q.Fragment},
|
|
||||||
{"in_top5", ok},
|
|
||||||
})
|
|
||||||
jsDetails = append(jsDetails, map[string]any{
|
|
||||||
"q": q.Query, "fragment": q.Fragment, "in_top5": ok,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
n := len(rank.EvalQuestions)
|
|
||||||
recall := 0.0
|
|
||||||
if n > 0 {
|
|
||||||
recall = float64(recalled) / float64(n)
|
|
||||||
}
|
|
||||||
passed := recall >= rank.EvalRecallThreshold
|
|
||||||
if jsonOut {
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
_ = enc.Encode(map[string]any{
|
|
||||||
"recall@5": round3(recall),
|
|
||||||
"passed": passed,
|
|
||||||
"gate": n,
|
|
||||||
"details": jsDetails,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
out := Dict{
|
|
||||||
{"recall@5", round3(recall)},
|
|
||||||
{"passed", passed},
|
|
||||||
{"gate", n},
|
|
||||||
{"details", details},
|
|
||||||
}
|
|
||||||
fmt.Print(toYAML(out, 0))
|
|
||||||
}
|
|
||||||
if !passed {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupLeaf(id string) (map[string]string, string, error) {
|
|
||||||
if conn == nil {
|
|
||||||
return nil, "", fmt.Errorf("brain not open")
|
|
||||||
}
|
|
||||||
stmt, err := conn.Prepare(
|
|
||||||
"MATCH (l:Leaf {id:$id}) RETURN l.id, l.text, l.root, l.confidence, l.source, l.type",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
res, err := conn.Execute(stmt, map[string]any{"id": id})
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
if !res.HasNext() {
|
|
||||||
return nil, "", fmt.Errorf("no leaf %s", id)
|
|
||||||
}
|
|
||||||
row, err := res.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
vals, err := row.GetAsSlice()
|
|
||||||
if err != nil || len(vals) < 6 {
|
|
||||||
return nil, "", fmt.Errorf("leaf row")
|
|
||||||
}
|
|
||||||
meta := map[string]string{
|
|
||||||
"id": fmt.Sprint(vals[0]),
|
|
||||||
"root": fmt.Sprint(vals[2]),
|
|
||||||
"confidence": fmt.Sprint(vals[3]),
|
|
||||||
"source": fmt.Sprint(vals[4]),
|
|
||||||
"type": fmt.Sprint(vals[5]),
|
|
||||||
}
|
|
||||||
return meta, fmt.Sprint(vals[1]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func leafStats() (map[string]any, error) {
|
|
||||||
if conn == nil {
|
|
||||||
return nil, fmt.Errorf("brain not open")
|
|
||||||
}
|
|
||||||
res, err := conn.Query("MATCH (l:Leaf) RETURN l.root, count(*)")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
byRoot := map[string]int{}
|
|
||||||
total := 0
|
|
||||||
for res.HasNext() {
|
|
||||||
row, err := res.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
vals, err := row.GetAsSlice()
|
|
||||||
if err != nil || len(vals) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n := int(asInt(vals[1]))
|
|
||||||
byRoot[fmt.Sprint(vals[0])] = n
|
|
||||||
total += n
|
|
||||||
}
|
|
||||||
return map[string]any{
|
|
||||||
"total": total,
|
|
||||||
"by_root": byRoot,
|
|
||||||
"db": dbPath(),
|
|
||||||
"model": ModelID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func clip(s string, n int) string {
|
|
||||||
if utf8.RuneCountInString(s) <= n {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return string([]rune(s)[:n])
|
|
||||||
}
|
|
||||||
|
|
||||||
func round3(f float64) float64 {
|
|
||||||
return float64(int(f*1000+0.5)) / 1000
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ bin/brain/search.go "onlyoffice postgres" --root facts # restrict to confirmed
|
|||||||
bin/brain/search.go "where is cs-lexicon" --json | yq '.[].ref'
|
bin/brain/search.go "where is cs-lexicon" --json | yq '.[].ref'
|
||||||
bin/brain/get.go <id> --body # full chunk only when needed
|
bin/brain/get.go <id> --body # full chunk only when needed
|
||||||
bin/brain/stats.go # index health
|
bin/brain/stats.go # index health
|
||||||
bin/brain/eval.go # recall@5 >= 0.95 gate (Go; Python bin/kb/eval is CI fallback)
|
bin/brain/eval.go # recall@5 >= 0.95 gate
|
||||||
```
|
```
|
||||||
|
|
||||||
`bin/kb/search` is a deprecated wrapper. `--hop` errors (File/FROM_FILE edges
|
`bin/kb/search` is a deprecated wrapper. `--hop` errors (File/FROM_FILE edges
|
||||||
|
|||||||
Reference in New Issue
Block a user