Skip to content

Fix search ranking and --hop, make the evidence rule and CI gates actually check something - #2

Open
debuggerone wants to merge 19 commits into
eSlider:mainfrom
debuggerone:fix/evidence-and-search-gates
Open

Fix search ranking and --hop, make the evidence rule and CI gates actually check something#2
debuggerone wants to merge 19 commits into
eSlider:mainfrom
debuggerone:fix/evidence-and-search-gates

Conversation

@debuggerone

Copy link
Copy Markdown

Started from three bugs in the Go search and ended up in the evidence model,
because each fix uncovered a gate that could not fail.

Search (bin/kbsearch)

  • FTS ranked backwards. QUERY_FTS_INDEX used ORDER BY score where
    kblib has always used ORDER BY score DESC, so the Go search took the
    worst BM25 matches and fed them into RRF in reverse rank order.
  • Nil dereference. SET STREAM_SANDBOX ran on the package-level conn
    before OpenConnection; any run with KBTEST_EPS set panicked.
  • --root returned nothing. Results were truncated to -n before the
    root/repo filters, so every matching leaf below the cut was discarded —
    --root facts came back empty whenever info leafs filled the top N. The
    deduction order was unreachable from the CLI.
  • The daemon was never started. ensureDaemon() existed and was never
    called, so every query loaded the ~100MB potion matrix in-process — the
    thing the daemon exists to avoid. It now auto-starts in its own session,
    with KBSEARCH_NO_DAEMON=1 to opt out.

--hop existed only in the docs

Documented in six files, implemented in none, and it failed silently: the
parser dropped unrecognised -flags but kept their arguments, so the
documented example bin/kb/search "what runs on arc-2" --hop 1 searched for
"what runs on arc-2 1".

Implementing it surfaced the reason nobody noticed: the graph had no
edges.
init_schema declared FROM_FILE, HAS_VERSION and RUNS_ON,
kblib advertised "Cypher graph hops", and a fresh index produced 47 Leaf
nodes, 0 File nodes and 0 edges. kb/index and mail/index_mail now write
File nodes and FROM_FILE edges (47 leafs → 12 files → 47 edges), and
--hop N walks them, tagging results with the depth they were reached at.
Unknown flags are now an error instead of being absorbed into the query.

The evidence rule was never checked

facts/audit db verified that " x " appeared in Leaf.source. That is a
claim about a string, not about evidence — "bullshit x bullshit2" passed
it. And the string was all there was: the schema flattened the sources into
one free-text field.

  • A source is now structured (kind, method, locator, origin) and two
    sources count as independent only when kind and origin both differ.
    Two compose files are two paths but one kind of claim — corroboration, not
    independence. Every source needs a locator, because evidence you cannot go
    back and look at is not evidence.
  • Evidence is stored as (Evidence)-[:SUPPORTS]->(Leaf), shared rather than
    copied, so "how many independent things did we actually look at" stays
    answerable. audit db counts observations, kinds and origins; the source
    string is no longer consulted by any check.
  • facts/crm used to record loc="bin/facts/crm" — the tool answering
    "where did you see it" with its own path. It now points at the CRM export
    key and the yaml org id.

Verified by injecting two forged facts into a real database: both are
caught, the blunt one (evidence=0) and the subtle one (two compose files,
2 evidence nodes and 2 origins but kinds=['declared']).

CI gates were fail-open

./bin/facts/audit self 2>/dev/null || echo "audit: not yet implemented; gate skipped"
./bin/kb/eval 2>/dev/null || echo "eval: not yet implemented; gate skipped"

kb/eval had in fact never run: the step used the system python instead
of uv run, died on import ladybug, and the || branch reported it as
"not yet implemented". Even with uv it would have failed — nothing built an
index and var/ is gitignored. Its control set also asked about "DevOps",
which appears nowhere in the repo corpus, pinning recall at 0.667.

Both gates now run through uv run with no || and no 2>/dev/null, an
index is built first (HF model from actions/cache, ~1GB cold), and the
control questions are ones the repo corpus can answer.

audit self was three greps

It checked that PLAN.md contained "recall@5" and that README mentioned
HNSW/BM25 — with 2.source as a regex, unescaped dot. It could not go red.
It now runs the pairing rule against a fixture, checks the tool convention
(D14), and verifies that every bin/… shown in a doc code block exists
and accepts the flags it is shown with
. That last check is what would have
caught --hop; the narrower version I wrote first did not.

Findings it produced, all fixed here: bin/md/tables and bin/brain/deduce
advertised in AGENTS.md do not exist, skills/diataxis-docs documented a
--type filter and --hop following related: links that were never
implemented, and bin/md/import had no usage block.

One decision left to the maintainer

skills/agent-cost documents bin/agents/cost, which was never vendored
into this repo (PLAN D2). Rather than delete a skill, it is declared in
EXTERNAL_TOOLS with a reason and printed under exceptions on every
audit self run:

exceptions:
  - "bin/agents/cost: skills/agent-cost documents the upstream tool; not
     vendored into this repo (PLAN D2). Vendor it or drop the skill."

An invisible suppression is how --hop survived in six files. Vendor the
tool or drop the skill — either way the exception should go.

Verification, and its limits

Green: 96 python tests, audit self and audit db clean, recall@5 1.0
after a rebuild from an empty var/, go vet + go test ./... on the root
module.

Not verified, stated plainly:

  • bin/kbsearch was never compiled. It needs the native ladybug library
    (lib-ladybug/, gitignored) which was not available. I built stub modules
    for the two cgo dependencies and ran go vet and the package tests over
    every real source file — that is how an unused strings import left by
    this change was caught — but the Cypher in hopStmt has never executed.
    Its python counterpart kblib.neighbours_of is tested against a real
    database.
  • bin/facts/crm was never run: neither /tmp/opencode/crm/graph.json
    nor the knowledge-mesh yaml exists outside its own environment. Reviewed
    and syntax-checked only.
  • The nested bin/kbsearch module is still outside CI for the same reason,
    recorded as OQ5. OQ6 records that Evidence.value_hash is written
    empty and nothing re-runs a locator, so confirmed still means "was true
    when observed" and staleness cannot be detected.

Happy to split this into separate PRs (search / evidence model / CI+audit)
if that reviews better — the commits are already grouped that way.

🤖 Generated with Claude Code

eSlider and others added 19 commits August 10, 2026 23:22
…creds

- bin/facts/crm: prove person<->company/company<->project against ooCRM
  x corpus SoT (knowledge-mesh-seed.yaml), write 78 facts (root=facts)
- tools/crmfacts.py + test_crm_facts.py: parser under unit tests (26 pass)
- docs/crm-associations-proof.md: provable graph, mistakes, fixes
- oo merge 759->763 resolves duplicate GoldenRatio.Exchange legal entity
- bin/db/ssh-tunnel: "$0" self-check + accept-new/BatchMode ssh flags
- AGENTS.md: document bin/facts/crm
Move serve/ (module) -> bin/server, tools/ -> bin/tools, replace bin/kb-watch
bash with bin/watch Go package; self-executing Go shebangs bin/serve.go and
bin/kb/watch.go; Docker + CI + git/import + docs repointed. Multi-stage image
builds static serve+watch binaries (no Go runtime in container).
- bin/mail/sync.go: async Go sync engine (8 workers, paginated Gmail via
  API + OnlyOffice IMAP); Gmail attachments key off body.attachmentId, not
  MIME partId; ICS sidecars Latin-1->UTF-8 normalized (TestICSToMarkdownNormalizesLatin1)
- bin/mail/import: message.json -> markdown; PDFs via pdftotext -layout
  fast path with docling subprocess fallback for the ~5% textless files
- bin/mail/index_mail: fresh-rebuild indexer (repo corpus + mail) avoiding
  ladybug WAL corruption on bulk-insert into indexed DBs; split from import
- bin/kb/index: keep FTS/VECTOR indexes across incremental runs (drop+recreate
  leaves stale backing tables killing the vector index)
- docs: README/PLAN/AGENTS cover the mail pipeline

Result: 17,835 messages -> 28,918 info leafs, FTS+HNSW healthy.
- New nested module bin/kbsearch with Go implementation of bin/kb/search
- Embedding model (potion-multilingual-128M) served by localhost daemon
  so repeated CLI calls reuse the loaded model
- Bash launcher bin/kb/search builds binary on first run, caches to var/bin/
- Hybrid FTS + vector search (RRF k=60) matching Python kblib behavior
- YAML output via port of yamlout.py (ordered keys, same format)
- JSON output with proper field order
- All flags: --root, --repo, -n, --json, --list-model
- Root go.mod reverted to 1.25.0 (kbsearch is isolated nested module)
- CI passes: go test ./... and go vet ./... unaffected by kbsearch
- PLAN.md: replace 'curasoft-detective' with 'detective method'
- README.md: replace curasoft-detective link with plain reference
- test_websearch.py: fix test domain from ticket.curasoft.de to example.com
- Rewrote git history with git-filter-repo to remove all traces
QUERY_FTS_INDEX was ordered `ORDER BY score LIMIT $n`, i.e. ascending, so
the Go search took the *worst* BM25 matches and fed them into the RRF fusion
in reverse rank order. kblib.py has always used `ORDER BY score DESC`.

Both Cypher statements now live in named consts next to each other so the
FTS (descending BM25) vs vector (ascending cosine distance) asymmetry is
visible in one place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
openWithOpts ran `SET STREAM_SANDBOX` on the package-level `conn` while it
was still nil — OpenConnection happened afterwards. Any run with KBTEST_EPS
set panicked instead of opening the brain.

Also: drop the `allow` parameter (never read, hence the rename to
openWithSandbox) and close the database on the error paths so a failed
extension load doesn't leak the open handle.

Not covered by a test: everything in this function needs the native ladybug
library, which the nested kbsearch module cannot build offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
runSearch fused the FTS and vector hits, cut the list to -n, and only then
applied --root/--repo. Every matching leaf ranked below the cut was thrown
away before the filter ever saw it, so `--root facts` returned nothing as
soon as info leafs filled the top N — the deduction order (facts first) was
unreachable from the Go CLI.

Fusion + filtering now live in rank.go as rankAndFilter(): fuse everything,
filter, then truncate. hybrid() takes limit <= 0 for "keep all" and breaks
RRF ties by id, so the output no longer depends on map iteration order.

rank.go has no cgo dependency; rank_test.go covers filter-before-limit for
both filters, the RRF fusion order and the tie determinism. Verified with a
stub module (the package itself needs the native ladybug lib to build).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ensureDaemon() was written but never called: every query fell straight
through to the in-process fallback and loaded the ~100MB potion matrix
again, which is exactly what the daemon exists to avoid.

embedQuery now tries the daemon, starts one in the background when nothing
answers, and retries once before falling back. The daemon gets its own
session (Setsid) so a Ctrl+C in the launching terminal does not kill it with
the foreground process group. KBSEARCH_NO_DAEMON=1 opts out for one-shot
containers and CI.

Verified against a stub-model build of the daemon half: cold run spawns the
daemon and answers, the daemon survives the CLI exit (health 200), the next
run reuses it, and the opt-out path stays in-process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ules.py

The evidence rule (D8) lived inline in facts/extract, tangled with docker
subprocess calls and file reads, so nothing could test it and facts/audit
could not re-check it.

factsrules is pure: observations in, Facts out. make_fact() refuses fewer
than 2 *distinct* sources, so a single observation cannot reach the facts
root by accident. Compose candidates are now scoped per container — the flat
{file: services} shape I started with would have paired a container `db`
against an unrelated project's compose.yaml that also declares `db`, which
is not a second source at all.

extract keeps collecting the observations; behaviour is unchanged, verified
by diffing --dry-run --json output of both versions against a fixture ssh
config (byte identical, 2 facts).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`audit self` was three greps over PLAN.md and README.md — and its own
docstring described the `db` mode ("every fact in db has >=2 sources"),
which self never did. The regex `2.source` had an unescaped dot, so it
matched almost anything. It could not go red.

It now checks what it claims to:

- evidence rule: runs factsrules against a fixture with one paired and one
  unpaired observation, asserts the single-source one stays out, every fact
  carries >=2 distinct sources in the " x " form facts/audit db greps for,
  and make_fact still refuses a single source
- tool convention (D14): shebang on line 1 (both `#!` and the Go
  `//usr/bin/env go run` form) and a usage line naming the tool
- documented modes: AGENTS.md/PLAN.md must list exactly the modes argparse
  accepts, which is now a single MODES constant
- the doc greps stay, with the two-source regex tightened to ">=2 sources"

Against the current tree it reports 5 real problems (bin/md/import has no
usage line; AGENTS.md and PLAN.md both advertise facts|info|stale, which do
not exist). Those are fixed in the next commit, so the gate goes green on
substance rather than by weakening it.

test_facts_audit.py exercises every check against a fixture tree that
violates it and one that satisfies it — a gate that cannot fail is the bug
being fixed here. The evidence rule itself was TDD via test_factsrules.py;
these checker tests were written after the checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes the 5 problems `audit self` reported:

- bin/md/import had a shebang and no usage block (D14); added one, including
  the exit codes it already returns
- AGENTS.md and PLAN.md advertised `facts|info|stale`; the tool has `self`
  and `db`. Both now say so
- docs/design.md described `bin/facts/audit stale` as if it existed; now
  phrased as planned and marked not implemented
- README's diagram called audit "confidence + staleness"; it does neither,
  it gates the 2-source rule and the tool convention
- the audit docstring described `db` behaviour under `self`, and claimed db
  checks source_rev, which it does not — it checks source, loc, how and
  confidence

audit self is green again on substance: 0 problems, 67 python tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mode check only looked at AGENTS.md and PLAN.md, which is why
docs/design.md could advertise `bin/facts/audit stale` unnoticed for as long
as it did.

It now scans README.md and docs/*.md too, and only inside code spans and
fenced blocks — prose like "run bin/facts/audit before pushing" would
otherwise be read as a mode named 'before'. Two shapes are checked: a
bracket list must be complete, a bare invocation must name a real mode.

Verified by appending `bin/facts/audit stale` to docs/design.md: audit self
goes red with exactly that finding, green again once reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both gates were `./tool 2>/dev/null || echo "not yet implemented; gate
skipped"`, so any failure was swallowed and the traceback hidden.

kb/eval had in fact never run in CI: the step invoked ./bin/kb/eval with the
system python instead of `uv run`, so it died on `import ladybug` and the ||
branch reported it as "not yet implemented". Even with uv it would have
failed — nothing builds an index in CI and var/ is gitignored.

- both steps now run through `uv run` with no `||` and no `2>/dev/null`
- a `bin/kb/index --rebuild` step builds the repo corpus first, with the HF
  model restored from actions/cache (~1GB on a cold cache)
- the control set drops `("eslider devops engineer", "DevOps")`: "devops"
  appears nowhere in the repo corpus, that question needs the portfolio
  corpus, and it alone kept recall at 0.667 — the gate could not have passed.
  Six questions that the repo corpus does answer replace it
- hit_texts_of no longer swallows exceptions; a broken FTS index must fail
  loudly instead of masquerading as recall 0.0
- PLAN.md records OQ5: the Go search path is still ungated because
  bin/kbsearch needs the native ladybug library, and states that gates are
  fail-closed by policy

Verified locally end to end after `rm -rf var`: audit self ok, index 47/47
leafs, recall@5 1.0, 70 python tests, go vet + go test green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntion

`" x " in source` accepted "bullshit x bullshit2", and counting distinct
strings accepted two compose files as two sources. Two compose files are two
paths but one method, one kind of claim, often one author — corroboration,
not independence.

A Source is now structured: kind (from a fixed taxonomy), method, locator,
origin. Two sources are independent only when kind *and* origin differ, and
every source needs a locator, because evidence you cannot go back and look
at is not evidence. check_independence() returns the reasons; make_fact()
refuses and says which rule broke.

Follow-on effects:

- facts/extract records *where* a doc mentions a host ('README.md:89'), so
  loc leads back to the evidence instead of asserting it exists
- repo compose pairings are keyed by file, so the locator names the compose
  that actually declares the service
- audit self now feeds five bad pairings through make_fact and fails if any
  is accepted — a gate that only accepts is not a gate
- Leaf.source keeps the " x " rendering for output, but it is derived from
  the sources and nothing verifies against it any more

Next commit persists the sources as Evidence nodes; until then audit db
still reads the flattened string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
audit db checked `" x " in source` — a claim about a string, not about
evidence. `"bullshit x bullshit2"` passed it. The string was all there was:
the schema flattened the sources into one free-text field, so nothing better
could be checked.

Evidence is now a node:

  (Evidence {id, kind, method, locator, origin, value_hash, observed_at})
      -[:SUPPORTS]-> (Leaf)

Evidence is shared, not copied — one observation backing two assertions is
one node with two edges, so "how many independent things did we look at"
stays answerable. kblib.facts_lacking_independence() counts observations,
kinds and origins per facts leaf and is what audit db now reports; the
source string is no longer consulted by any check.

facts/extract and facts/crm both write structured evidence. crm's `loc` used
to be "bin/facts/crm" — the tool answering "where did you see it" with its
own path; it now points at the CRM export key and the yaml org id.

Verified end to end against a real db: extract writes 2 facts, audit db is
clean, then two forged facts are injected and both are caught —
"bullshit x bullshit2" (evidence=0) and the subtle one, two compose files
with 2 evidence nodes and 2 origins but kinds=['declared']. 82 tests pass.

facts/crm could not be run: neither /tmp/opencode/crm/graph.json nor the
knowledge-mesh yaml exists on this machine, so its change is reviewed and
syntax-checked only.

Docs updated (AGENTS, PLAN D8, README, design.md) and PLAN OQ6 records what
is still missing: value_hash is written empty, nothing re-runs a locator, so
`confirmed` still means "was true when observed".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph was a bag of leafs. init_schema declared File, FROM_FILE,
HAS_VERSION and RUNS_ON, kblib's docstring advertised "Cypher graph hops",
and a fresh index produced 47 Leaf nodes, 0 File nodes and 0 edges — every
rel table empty. Nothing that reads the graph could have returned anything.

upsert_leaf now takes file_path/repo and MERGEs a File node plus a
FROM_FILE edge; kb/index and mail/index_mail pass them. neighbours_of()
does one hop: the other leafs of the same file, which is the step `--hop N`
repeats from each new frontier.

After --rebuild: 47 leafs, 12 files, 47 edges. A leaf found by FTS in
README.md now walks to its 6 siblings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--hop was documented in six files and implemented in none. Worse, it failed
silently: the parser dropped any unrecognised `-flag` and kept its argument,
so the documented example `bin/kb/search "what runs on arc-2" --hop 1`
searched for "what runs on arc-2 1". Proven by running the old parse loop
verbatim.

Parsing moved to args.go, free of cgo so it can be tested at all. Unknown
flags, non-numeric or out-of-range values and an empty query are now errors
with exit 2 and a usage line, instead of being absorbed into the query.

expandHops() walks the FROM_FILE edge: each round asks for the neighbours of
the previous frontier, skips leafs already seen (so a<->b cannot loop), tags
new ones with their depth and adds at most `-n` per round. Hits carry
`hop: N` in YAML and JSON; ranked hits stay unmarked.

Docs corrected to what is actually walked: design.md and kb-search/SKILL.md
claimed `related:` links and vector-neighbours, and no such edges are written.

Verification: the package now type-checks as a whole. bin/kbsearch cannot be
compiled here (no native ladybug), so I built stub modules for the two cgo
dependencies and ran `go vet` plus the tests over every real source file --
which is how the unused `strings` import left behind by this change was
caught. The Cypher in hopStmt mirrors kblib.neighbours_of, which is tested
against a real database in test_kblib.py; the Go query itself is unrun.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s modes

The mode check I added only covered bin/facts/audit, so it would not have
caught --hop, and did not. check_documented_commands() now scans AGENTS.md,
PLAN.md, README.md, docs/*.md and skills/*/SKILL.md: every bin/... shown
inside code must exist, and every flag it is shown with must be declared in
its source.

Static by design — running each tool with --help would execute tools like
facts/crm that have no argparse and no dry-run. Both declaration spellings
count, since argparse writes "--rebuild" while Go's flag package writes
"out" and still accepts --out.

Three false positives were found while triaging and fixed in the checker,
not by loosening the rule: `-s bin/tools` is a directory argument, `-n` in
`bash -n bin/db/psql-yq` sits before the command, and `///usr/bin/env go run`
is not an invocation of `bin/env`.

Four real findings, now fixed:

- AGENTS.md advertised bin/md/tables and bin/brain/deduce; neither exists.
  Replaced with bin/kb/stats, and PLAN's layout marks the rest as planned
- skills/diataxis-docs documented `--hop` following `related:` links and a
  `--type` filter; neither exists. Corrected to what the tools do
- skills/agent-cost documents bin/agents/cost, which was never vendored.
  Rather than delete someone's skill, it is declared in EXTERNAL_TOOLS with
  a reason and printed under `exceptions` on every audit run — an invisible
  suppression is how --hop survived in six files

96 python tests, audit self and db clean, recall@5 1.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@debuggerone

Copy link
Copy Markdown
Author

The one finding this PR does not fix — skills/agent-cost documenting bin/agents/cost — is filed separately as #3, since dropping a skill is a maintainer's call. This PR only declares it as a visible exception in the meantime.

@eSlider eSlider self-assigned this Aug 13, 2026

@eSlider eSlider left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: request changes

This PR found that several gates were theatre and replaced them with checks that can actually go red. The FTS ascending-score bug, filter-before-limit, unknown flags being swallowed into the query, and " x " in source were all real. The tests name the failure mode. OQ5/OQ6 are written as open questions instead of papered over. That is the right standard for this repo.

Blocking before merge (AGENTS.md + PLAN D8/D15):

  1. No absolute /mnt/ paths in committed files or in persisted evidence. bin/facts/crm now interpolates /mnt/8TB/... and /tmp/opencode/... into Evidence locators/origins.
  2. File identity is path-only. Two repos with README.md share one File node; --hop then walks across repos.
  3. The ranking/parser tests do not run in CI. rank.go/args.go sit in the cgo module. The thesis of this PR is that a gate must be able to fail — these tests need a cgo-free package so go test on ubuntu-latest actually runs them.
  4. --hop drops --root/--repo and treats a DB error as exit 0. Same class of silent failure this PR exists to kill.
  5. bin/facts/audit module docstring still describes the " x " source check the rest of the PR removed (rule 6: docs reflect behaviour).

Also:

  • Drop the "Generated with Claude Code" footer from the PR body. Commits carry Co-Authored-By: Claude Opus 5; squash-merge can drop those trailers. This repo does not attribute PRs/commits to AI.
  • CI has not reported on this fork PR yet. First-time contributor workflows often need maintainer approval — please trigger the Checks tab after addressing the above.

Happy to re-review a follow-up that keeps the evidence model and the fail-closed CI. The commit grouping (search / evidence / CI+audit) is already the right split if this needs to land in pieces.

Comment thread bin/facts/crm
f"{CRM_GRAPH}:companies_with_persons:{item['crm_key']}", "oo-crm"),
factsrules.Source(
"doc", CORPUS_MESH.name,
f"{CORPUS_MESH}:orgs:{item['org']}", f"file:{CORPUS_MESH}"),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — GitHub safety (AGENTS.md rule 1) and PII.

CORPUS_MESH is still /mnt/8TB/projects/eslider/cv/.... This change now writes that absolute path (and /tmp/opencode/crm/graph.json) into Evidence.locator / Evidence.origin, so a later audit db dump or leaf export leaks a host path into the graph.

The old loc="bin/facts/crm" was the wrong kind of pointer; this is the right kind with the wrong value.

  • Resolve both inputs from env ($PROJECTS_ROOT, $HOME, or a gitignored config path). Do not commit /mnt/, /home/<user>/, /Users/<user>/.
  • Store locators as repo-relative / logical keys (knowledge-mesh-seed.yaml:orgs:<id>, oo-crm:companies_with_persons:<key>), not host paths.
  • The fact text still interpolates CRM person names into root=facts on a public repo. That was pre-existing, but this path now persists it as structured evidence — keep third-party names out of committed fixtures and prefer ids over names in leaf text.

Also: json.load(open(CRM_GRAPH)) is still an unclosed handle.

Comment thread bin/tools/kblib.py
Without these edges the graph is a bag of leafs: `--hop` has nothing to
walk and the File->Commit->Person history has nothing to hang off.
"""
fid = sha256_b64(path)[:24]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — File identity ignores repo.

fid = sha256_b64(path)[:24] hashes the path only. repo is stored as a property and then overwritten on MERGE.

The brain indexes multiple corpora (repo markdown, mail, ops). Two README.mds (or the same relative path in mail vs tree) collapse onto one File node. --hop then returns siblings from the other repo — the walk this PR just made real.

Include repo in the id (sha256(f"{repo}:{path}")), and do not MERGE-overwrite f.repo.

Secondary: mtime here is the observation clock from upsert_leaf, not the file's mtime. That will make D10/HAS_VERSION staleness lie. Pass the real mtime (or leave it unset until git/import owns it).

Comment thread bin/tools/kblib.py
found = evidence_for(conn, lid)
kinds = sorted({e["kind"] for e in found if e["kind"]})
origins = sorted({e["origin"] for e in found if e["origin"]})
if len(found) >= MIN_EVIDENCE and len(kinds) >= MIN_EVIDENCE and len(origins) >= MIN_EVIDENCE:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audit db does not enforce locators, unlike make_fact.

check_independence refuses an empty locator. This gate only counts nodes / distinct kinds / distinct origins. A writer that bypasses make_fact (or upsert_evidence(..., locator="")) can store two kind+origin pairs with nowhere to re-check, and audit db stays green.

The PR's own rule: evidence you cannot go back and look at is not evidence. Count locators here too (and reject empty kind/origin rather than dropping them from the set).

Thank you for killing " x " in source — the injected bullshit x bullshit2 case is exactly the test this needed.

Comment thread bin/facts/audit
Exit 0 = all checks pass, 1 = audit failures, 2 = could not evaluate.
`db` loads every Leaf with root=facts and asserts each carries a two-source
`source` (the " x " pairing), a non-empty `loc` (the "where did you see it"
pointer), a `how`, and confidence='confirmed'.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs reflect behaviour (AGENTS.md rule 6).

This docstring still says db asserts a two-source source (the " x " pairing). The function below deliberately stopped doing that — that was the point of the PR.

Readers of the tool file will re-learn the bug you just removed. Update this block to: Evidence nodes, ≥2 observations, ≥2 kinds, ≥2 origins, locator present.

Comment thread bin/kbsearch/search.go
walked, err := expandHops(results, opt.hops, limit, neighbours)
if err != nil {
fmt.Fprintf(os.Stderr, "hop: %v\n", err)
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same class of silent failure this PR exists to kill.

  1. Hop DB errors are printed to stderr, then results = walked and runSearch returns 0. The user gets a successful YAML document with no hop key and no non-zero exit. Unknown flags now fail closed (good); this path is still fail-open.
  2. rankAndFilter honours --root/--repo, then expandHops appends every sibling of the file, including the other root. bin/kb/search "q" --root facts --hop 1 will mix info leafs back in. Deduction is facts first; the filter has to apply to hopped hits too (or be documented as intentionally unfiltered, with a test).

hopStmt itself is fine as a mirror of kblib.neighbours_of (tested in Python). The Go []any bind has never executed — if ladybug rejects the param type, this branch will hit (1) on every --hop. Return non-zero.

Comment thread bin/kbsearch/rank_test.go
h("i3", "info", "docs/c.md"),
h("f1", "facts", "docker ps x compose"),
}
eq(t, rankAndFilter(fts, nil, "facts", "", 2), "f1")

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. This is the test the old hybrid+truncate+filter sequence could not have passed. The comment states the failure mode in one sentence. Same for TestParseHopIsNotSwallowedIntoTheQuery and the RRF tie-break.

Please get these onto CI (cgo-free package — comment on ci.yml). A perfect regression test that never runs is how --hop survived in six docs.

Comment thread bin/kbsearch/args.go
opt.listModel = true
default:
if strings.HasPrefix(arg, "-") {
return opt, fmt.Errorf("unknown flag %q", arg)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Unknown flags as an error with exit 2 is the correct fix for "documented --hop 1 searched for query 1". Validating --root / -n / --hop instead of silently keeping the default is the same standard.

Comment thread bin/tools/factsrules.py
f"sources share one origin ({sorted({s.origin for s in sources})}); "
"one system of record cannot confirm itself"
)
return problems

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. This is the actual D8 rule: kind and origin, locator required, taxonomy closed, make_fact refuses instead of rendering a string that looks like two sources.

The pairing helpers staying pure (no docker, no db) is why audit self can run in CI. The "two compose files are corroboration" refusal is the one that would have been impossible to express with " x " in source.

Comment thread bin/kb/eval
except Exception:
return []
# Deliberately unguarded: a broken FTS index must fail the gate loudly,
# not disguise itself as recall 0.0.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Unguarded FTS is the right default: a missing index must fail the gate, not report recall 0.0. Control questions that the repo corpus can actually answer were the other half of this fix.

Comment thread bin/kbsearch/search.go
cmd.Stderr = nil
// Own session: a Ctrl+C in the terminal that launched the search must not
// take the daemon down with the foreground process group.
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for actually calling ensureDaemon and for Setsid so Ctrl+C on the search does not kill the server.

Please also Release() the child after Start (otherwise the CLI holds a zombie until it exits), and serialise the bind: two concurrent searches can both miss /health and both spawn serve on 17830. First wins, second dies after 10s and falls through to a 100MB in-process load — which is the path this change was meant to avoid.

@eSlider

eSlider commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Rebase required — current main has moved

The review above was on the PR diff. Checking against current main (fe6a020) changes the merge decision.

This branch is based on 1d1f6a9 and does not contain:

  • b73b4d4fix(kb): stop DROP INDEX killing HNSW via Ladybug ghost catalog
  • ff1716d / 98c14e2 — absolute /mnt/ paths removed; bin/facts/crm already uses KNOWLEDGE_MESH_SEED / repo-relative fallback

So a merge as-is would regress two landed fixes:

  1. kblib.create_fts_and_vector / drop_indexes in this PR still DROP INDEX IF EXISTS Leaf.Leaf_*. On Ladybug 0.19 that leaves ghost catalog entries (_0_Leaf_vec_UPPER, 0_id_docs); CREATE then fails while SHOW_INDEXES omits the index. Current main made drop_indexes a no-op and forbids DROP. Keep that. Evidence/link_to_file/neighbours_of must land on top of current kblib.py, not replace it.
  2. bin/facts/crm here hardcodes /mnt/8TB/projects/eslider/cv/... and then writes that path into Evidence locators. Current main already dropped that constant. Rebase and keep the env-var path; locators must stay logical (knowledge-mesh-seed.yaml:orgs:<id>), not host paths.

Please rebase onto current main and re-run the python tests + audit self. The Go search fixes (FTS DESC, filter-before-limit, OpenConnection before SET, actually calling ensureDaemon) are still missing on main and are still wanted — they just cannot ship with the old kblib DROP path.

Do not mix with unrelated local WIP (LinkedIn chats / models/ weights).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants