feat: D16 adjudication — 2v2 stays hypothesis until a rule fires.
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (pull_request) Failing after 6s
Tests / OCR (tesseract fixture) (pull_request) Failing after 4s
Tests / Release (semver) (pull_request) Skipped

temporal_freshness then authority_pairing; unresolved keeps (not confirmed).
bin/facts/audit contradict. Gitea #29.
This commit is contained in:
2026-08-14 11:40:16 +01:00
parent a331042488
commit 2b8f946edb
18 changed files with 557 additions and 56 deletions
+46 -19
View File
@@ -1,14 +1,15 @@
#!/usr/bin/env python3
"""facts/audit - evidence & lexicon checks for the 2dph brain.
bin/facts/audit self # lexicon: every fact in db has >=2 sources
bin/facts/audit db # evidence gate: run against var/kb.lbug
bin/facts/audit self # lexicon: docs + two-source rule
bin/facts/audit db # evidence gate against var/kb.lbug
bin/facts/audit contradict # D16 adjudication (JSON claim(s) on stdin)
`self` mode checks the repo itself (no network, no runtime deps). It greps
for known-good two-source pairings and confirms the docs are consistent.
`db` mode loads every Leaf with root=facts and asserts each has source_rev
and a non-empty `loc` (the "where did you see it" evidence pointer) and that
'confirmed' facts carry a two-source `source` field.
`self` mode checks the repo itself (no network, no runtime deps).
`db` mode loads every Leaf with root=facts. Confirmed facts need ` x `;
hypothesis contradictions need `a x b vs c x d` (both sides ≥2).
`contradict` applies temporal_freshness then authority_pairing; ≥2 vs ≥2
with no rule stays hypothesis / `(not confirmed)`.
Exit 0 = all checks pass, 1 = audit failures, 2 = could not evaluate.
"""
@@ -22,6 +23,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "bin" / "tools"))
from contradict import adjudicate, check_fact_row # noqa: E402
def audit_db() -> list[str]:
from kblib import connect
@@ -33,14 +36,8 @@ def audit_db() -> list[str]:
r = conn.execute("MATCH (l:Leaf {root:'facts'}) RETURN l.id, l.source, l.loc, l.how, l.confidence")
problems: list[str] = []
for lid, source, loc, how, conf in r.get_all():
if conf != "confirmed":
problems.append(f"{lid}: facts require confidence='confirmed', got '{conf}'")
if not source or " x " not in source:
problems.append(f"{lid}: needs 2-source evidence in source, got '{source}'")
if not loc:
problems.append(f"{lid}: missing loc (evidence pointer)")
if not how:
problems.append(f"{lid}: missing how")
problems.extend(check_fact_row(str(lid), str(source or ""), str(loc or ""),
str(how or ""), str(conf or "")))
conn.close()
db.close()
return problems
@@ -54,20 +51,50 @@ def audit_self() -> list[str]:
problems.append("PLAN.md missing recall@5 gate")
if re.search(r"(?i)facts must have.*2 sources|2.source", plan) is None:
problems.append("PLAN.md missing the two-source evidence rule for facts")
if "temporal_freshness" not in plan or "authority_pairing" not in plan:
problems.append("PLAN.md missing D16 adjudication rules")
if re.search(r"(?i)HNSW|BM25|deduction", (ROOT / "README.md").read_text()) is None:
problems.append("README.md missing search/retrieval description")
return problems
def audit_contradict(raw: str) -> tuple[list[str], list[dict]]:
raw = raw.strip()
if not raw:
return ["contradict: empty stdin (JSON claim or {claims:[...]})"], []
try:
payload = json.loads(raw)
except json.JSONDecodeError as e:
return [f"contradict: invalid JSON: {e}"], []
if isinstance(payload, dict) and "claims" in payload:
claims = list(payload.get("claims") or [])
elif isinstance(payload, dict):
claims = [payload]
elif isinstance(payload, list):
claims = payload
else:
return ["contradict: expected object or list"], []
details = [adjudicate(c) for c in claims]
return [], details
def main(argv: list[str]) -> int:
import argparse
p = argparse.ArgumentParser(description="evidence & lexicon audit")
p.add_argument("mode", choices=("self", "db"))
p.add_argument("mode", choices=("self", "db", "contradict"))
p.add_argument("--json", action="store_true")
a = p.parse_args(argv)
problems = audit_self() if a.mode == "self" else audit_db()
out = {"mode": a.mode, "ok": not problems, "problems": problems}
details: list[dict] = []
if a.mode == "self":
problems = audit_self()
elif a.mode == "db":
problems = audit_db()
else:
problems, details = audit_contradict(sys.stdin.read())
out: dict = {"mode": a.mode, "ok": not problems, "problems": problems}
if details:
out["contradictions"] = details
if a.json:
print(json.dumps(out, indent=2))
else:
@@ -77,4 +104,4 @@ def main(argv: list[str]) -> int:
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
sys.exit(main(sys.argv[1:]))
+1
View File
@@ -5,6 +5,7 @@
//
// ./bin/facts/audit.go self
// ./bin/facts/audit.go db
// ./bin/facts/audit.go contradict --json < claim.json
//
// Python bin/facts/audit is the implementation (CI runs it directly).
// NOTE: never run `gofmt -w` on this file — it breaks the shebang.