cli: flip 'arborist query' default to providence_query; --legacy hatch

#000072 Phase 2 step 3. The user-facing 'arborist query' command now
routes through arborist.qa.providence_query (cache-aware run_query
wrapper) instead of the legacy 2000-line arborist.qa.query.query().
Legacy function stays alive in the module — other importers
(test_query.py, internal calls) keep working — but the CLI defaults
to the new orchestrator.

Escape hatch:
  arborist query "..." --legacy
  ARBORIST_LEGACY_QUERY=1 arborist query "..."

Either re-routes through the legacy retrieval+gate pipeline. Useful
when a themed bench subset regresses on the new path and operators
need fleet-wide fallback while the gap gets ported.

Why now: the legacy "dinosaur → Edwina" primary-source bug fixes
itself on the default path — slim FTS5 cloud parity already proved
the run_query orchestrator picks the canonical 'Dinosaur' article
on Q5. Smoke confirms it:
  arborist query "why did the dinosaurs go extinct?" --dry-run
    → primary: Dinosaur                       (default — new)
  arborist query "..." --legacy --dry-run
    → primary: Edwina, the Dinosaur Who Didn't Know She Was Extinct

Known gaps providence_query DOESN'T port today (legacy still has):
  - pre-retrieval: canonical_projection, crosslang sandwich,
    quantifier preflight, metacog, soft_preflight, frame_detection
  - post-retrieval: answerability, repair, witness, sandwich edge-out
  - merkle_proof column is "[]" placeholder; equivalence_class
    fallback lookup omitted
  - args legacy accepts that providence ignores: retrieval_keywords,
    extra_body, translator, fidelity, over_fetch

Forcing-function-style rollout per fox 2026-05-31 — the bench
regression-finding IS the next signal. Themed subsets in
bench/qa_questions.txt that exercise the missing gates may regress;
those are the targets for the next round of porting.

CLI changes:
  - new --legacy flag (with ARBORIST_LEGACY_QUERY=1 env equivalent)
  - _cmd_query branches on use_legacy → legacy query() vs builds
    Corpus + calls providence_query
  - providence_query result.status mapped fresh_persisted/burned →
    cache_miss_then_written so the bottom-of-function exit-code
    check stays consistent

providence_query.providence_query cache_hit branch now surfaces
raw_answer, verifier_method, n_quotes, n_verified, unverified_quotes,
violations so the render layer + journal emitter don't crash on
None when serving from cache.

283 tests pass in the broader gate.
This commit is contained in:
russell@unturf.com 2026-05-31 15:25:20 -04:00
parent 17b9622a08
commit 492d1a8e7b
No known key found for this signature in database
2 changed files with 112 additions and 18 deletions

View file

@ -644,25 +644,95 @@ def _cmd_query(args: argparse.Namespace) -> int:
cli_override=getattr(args, "progress_override", None),
)
result = query(
question=args.question,
qa_db=qa_db,
chat_client=client,
model_id=model,
revision=revision,
quantization=quantization,
shards_dir=shards_dir,
single_db=single_db,
extra_shards=extra_shards,
top_k=args.top_k,
over_fetch=args.over_fetch,
max_context_chars=args.max_context_chars,
policy=call_policy,
fidelity=getattr(args, "fidelity", None),
burn_existing=bool(getattr(args, "burn", False)),
retrieval_keywords=getattr(args, "retrieval_keywords", None),
progress=progress,
# #000072 Phase 2 step 3 — default to providence_query unless the
# caller opts back into legacy via --legacy or ARBORIST_LEGACY_QUERY=1.
# The legacy path keeps the full pre/post gate stack (canonical_
# projection, crosslang sandwich, quantifier guard, metacog,
# soft_preflight, answerability, repair, witness, sandwich edge-out);
# providence_query skips those today but routes through the unified
# run_query orchestrator that fixes the legacy "dinosaur → Edwina"
# primary-source bug on the smoke fixture. Bench-gated rollback: if
# themed subsets in bench/qa_questions.txt regress on providence,
# set ARBORIST_LEGACY_QUERY=1 in env for fleet-wide fallback.
use_legacy = (
getattr(args, "legacy", False)
or os.environ.get("ARBORIST_LEGACY_QUERY") == "1"
)
if use_legacy:
result = query(
question=args.question,
qa_db=qa_db,
chat_client=client,
model_id=model,
revision=revision,
quantization=quantization,
shards_dir=shards_dir,
single_db=single_db,
extra_shards=extra_shards,
top_k=args.top_k,
over_fetch=args.over_fetch,
max_context_chars=args.max_context_chars,
policy=call_policy,
fidelity=getattr(args, "fidelity", None),
burn_existing=bool(getattr(args, "burn", False)),
retrieval_keywords=getattr(args, "retrieval_keywords", None),
progress=progress,
)
else:
from arborist.qa.providence_query import providence_query
from arborist.qa.corpus import (
MultiShardSqliteCorpus, SqliteShardCorpus,
)
from arborist.store import connect as _connect
# Build Corpus from the same arg-shape legacy query() resolves:
# shards_dir wins when set (multi-shard), else single_db.
_all_shard_paths: list[Path] = []
if shards_dir:
candidates = sorted(Path(shards_dir).glob("*.db"))
skip = {"qa.db", "snapshots.db", "selfmodel-chain.db"}
_all_shard_paths = [p for p in candidates if p.name not in skip] or candidates
if extra_shards:
_all_shard_paths.extend(extra_shards)
if _all_shard_paths:
corpus_obj = MultiShardSqliteCorpus(_all_shard_paths)
_closer = corpus_obj.close
elif single_db:
_conn = _connect(single_db)
corpus_obj = SqliteShardCorpus(_conn)
_closer = _conn.close
else:
print(
"query needs --shards-dir or --db (or --extra-shards). "
"Use --legacy to fall back to the legacy retrieval pipeline.",
file=sys.stderr,
)
return 2
# Resolve per-mode max_context_chars when caller didn't set one
# — same shape legacy query() uses.
mcc = args.max_context_chars
if mcc is None:
by_mode = call_policy.get("max_context_chars_by_mode") or {}
answer_mode = call_policy.get("answer_mode", "quote")
mcc = int(by_mode.get(answer_mode, by_mode.get("quote", 24_000)))
try:
result = providence_query(
corpus_obj, args.question, client,
qa_db=qa_db,
policy=call_policy,
model_id=model,
burn_existing=bool(getattr(args, "burn", False)),
top_k=args.top_k,
max_context_chars=mcc,
)
finally:
try:
_closer()
except Exception:
pass
# Normalize providence_query status → legacy exit-code values
# so the bottom-of-function status check stays consistent.
if result.get("status") in ("fresh_persisted", "burned"):
result["status"] = "cache_miss_then_written"
# Emit unfirehose-compatible session journal. One JSONL file per
# `make query` invocation, written to ~/.arborist/unfirehose/{slug}/
@ -5817,6 +5887,20 @@ def build_parser() -> argparse.ArgumentParser:
"tools that misbehave on stderr noise."
),
)
query_cmd.add_argument(
"--legacy", action="store_true",
help=(
"Use the legacy 2000-line query() retrieval pipeline instead "
"of the unified providence_query orchestrator (default since "
"#000072 Phase 2 step 3). Legacy preserves the full pre/post "
"gate stack (canonical_projection, crosslang sandwich, "
"quantifier guard, metacog, soft_preflight, answerability, "
"repair, witness, sandwich edge-out) that providence_query "
"hasn't ported yet. Use --legacy when a themed bench subset "
"regresses on the new path. ARBORIST_LEGACY_QUERY=1 env var "
"has the same effect."
),
)
query_cmd.set_defaults(
func=_cmd_query, progress_override=None, witness_override=None,
)

View file

@ -369,15 +369,25 @@ def providence_query(
cached = _lookup(qa_conn, ckey)
if cached is not None and not burn_existing:
_bump_hit(qa_conn, ckey)
# Surface every field the legacy query() result carries
# so render layers + journal emitters don't crash on None.
return {
"status": "cache_hit",
"cache_key": ckey,
"lookup_path": "primary",
"audit_mode": cached["audit_mode"],
"answer_text": cached["answer_text"],
"raw_answer": cached["answer_text"],
"sources": sources,
"audit_event_hash": cached["audit_event_hash"],
"run_dag_root": cached["run_dag_root"],
"verifier_method": cached["verifier_method"] or "none",
"n_quotes": int(cached["n_quotes"] or 0),
"n_verified": int(cached["n_verified"] or 0),
"unverified_quotes": _json.loads(
cached["unverified_quotes"] or "[]"
),
"violations": [],
"burned_existing": 0,
"elapsed_s": round(_time.time() - t_total, 3),
}