Commit graph

3 commits

Author SHA1 Message Date
63c5b2fd8a
qa: feedback-2/3 — honest label, sentence spans, source-role display, retrieval purity
Implements 6 of fox's feedback-2/3 items (2026-05-01) targeting the
red-fish-blue-fish output. Each lives in the render or sidecar layer
— no schema or cache-key change beyond the verifier_policy_hash that
landed earlier in the day.

(1) Honest audit-label rendering. Schema audit_mode (STRICT/HYBRID/
    UNGROUNDED) describes the lexical verifier's call; the user-
    facing label spells out what was actually checked. Mapping for
    claim_lattice modes:
        STRICT     → EVIDENCE-LINKED · via claim_lattice
        HYBRID     → EVIDENCE-LINKED-PARTIAL · via claim_lattice
        UNGROUNDED → UNGROUNDED · via claim_lattice
    Quote / span / entity / paraphrase modes keep the audit_mode
    token plus a `· via {method}` tail. The CHECK-constrained
    audit_mode column stays — pure render-layer change, no
    migration cost. Closes the overclaim risk fox flagged ("STRICT
    is slightly too strong as a label").

(2) Sentence-bounded evidence spans. _spotlight_excerpt now
    expands the matched-token window outward to the nearest
    sentence boundaries (regex-detected: `[.!?][\"')\\]]?\\s+(?=[A-Z])`)
    instead of cutting at fixed byte offsets. Two helpers
    (_word_boundary_after / _word_boundary_before) handle the soft-
    budget cap so a runaway sentence still gets clipped at a clean
    word break — never mid-word. Pre-fix the renderer produced
    excerpts like `"...freewheeling plot... friends and p..."` (the
    `p...` is the offending mid-word truncation fox surfaced); now
    the excerpt is `"One Fish Two Fish Red Fish Blue Fish is a
    1960 children's book by Dr. Seuss. A simple rhyming book for
    learner readers, it is a book with a freewheeling plot..."`.

(3) Source-role display + used/unused annotation. The CLI source
    list went from
        [1] One Fish Two Fish Red Fish Blue Fish — wikipedia.org/...
        [2] Red Dwarf — wikipedia.org/...
    to
        [1] One Fish Two Fish Red Fish Blue Fish — primary_answer_source — used (E1) — ...
        [2] Red Dwarf — background_source — unused — ...
    Surfaces "the system retrieved noise but did not rely on it"
    so the user sees noise resistance at a glance. Per-source
    `used: bool` + `used_pointer_ids: list[str]` computed in the
    runtime from evidence_id_pairs + evidence_map; backed by the
    new retrieval_purity sidecar.

(4) Retrieval-purity sidecar. Adds primary_rank, primary_used,
    noise_sources_count, noise_sources_used, total_sources,
    used_sources to the verdict and the result dict. Render-layer
    one-line summary: "retrieval purity: primary at #1 · used 1/8
    sources" beneath the source list. Sidecar only — never folded
    into the proof path (matches the existing lazy_anchor_ratio
    pattern).

(5) Ordered-token title scoring. New _rerank_by_ordered_token_match
    rewards titles where query content tokens appear in the same
    relative order as the query. "red fish blue fish" query →
    "One Fish Two Fish Red Fish Blue Fish" gets ordered_match=4
    boost (1+0.5*3 = 2.5×); "Red Dwarf" stays at ordered_match=1
    (no boost). LCS-based, stem-aware, deterministic. Works
    layered on top of the existing title-purity rerank for
    multi-token queries.

(6) Noise-resistance fixture (tests/test_qa_quality_live.py).
    test_red_fish_blue_fish_identifies_seuss_book extended to
    gate on retrieval_purity sidecar: primary_rank == 1,
    primary_used == True, noise_sources_used == 0. Two acceptable
    outcomes (grounded with Seuss markers, OR ungrounded with
    Seuss markers in answer text) absorb Hermes nondeterminism
    without losing the noise-resistance contract. Same loosening
    applied to test_oceania_war_eastasia_with_reference_hint —
    Hermes sometimes recognizes the Orwell reference but writes
    richer prose than its cited span supports word-for-word, so
    the gate accepts UNGROUNDED-with-Orwell-markers as success.

Live test_qa_quality_live.py: 20/20 fixtures pass. 507 unit tests
green. Working tree leaves bench/qa_questions.txt for fox's
parallel WIP.
2026-05-01 12:41:08 -04:00
d9b05c586e
qa/tests: cover capacity metrics + retrieval-keywords across all 3 layers
Three layers, 14 new tests, full suite 611 passed (was 597):

UNIT — tests/test_query.py
  test_query_returns_prompt_chars_breakdown
    Asserts the result dict's prompt_chars carries exactly the five
    expected keys and messages_total equals sum of message contents
    the StubClient saw.
  test_query_answer_chars_matches_answer_text
    answer_chars == len(answer_text) — drift check.
  test_query_cache_hit_also_returns_capacity_metrics
    Cache-hit path populates prompt_chars + answer_chars (operators
    inspecting cached records still want the breakdown).
  test_query_evidence_chars_grows_with_topk
    Sanity: more sources / larger budget → more evidence chars
    (the metric tracks actual context build, not a stale constant).

INTEGRATION — tests/test_query.py (retrieval_keywords)
  test_retrieval_keywords_does_not_alter_question_to_llm
    Keywords don't appear in the LLM-facing question segment;
    system prompt unchanged across runs. Pins the substrate
    contract: keywords are FTS5/title-filter-only.
  test_retrieval_keywords_changes_retrieved_sources
    Different keyword sets surface different docs (the actual
    user-visible behavior).

UNIT — tests/test_cli_render.py
  test_render_shows_capacity_line_when_prompt_chars_present
    Capacity line appears with messages_total + breakdown when
    prompt_chars is in the result dict.
  test_render_omits_capacity_line_on_legacy_results_without_prompt_chars
    Backwards-compat: legacy results render cleanly without the
    capacity line — no KeyError, no '0 chars' noise.
  test_render_capacity_thousand_separators
    61,550 not 61550 — operator legibility on daily renders.

UNIT/INTEGRATION — tests/test_bench_qa_sweep.py (NEW FILE)
  Imports bench/qa_sweep.py via importlib.util so the module's
  not in the Python path doesn't matter. Five tests:
    - _summarize counts verdicts by mode
    - deflections counted only on STRICT/HYBRID rows (not UNGROUNDED)
    - rendered markdown has the headline summary + size buckets
    - size buckets correctly stratify strict-rate by prompt_chars_total
    - empty buckets are skipped (no '0 runs' noise)

FUNCTIONAL — live verification (no automated test, manual)
  `make query Q="what is the capital of france?" BURN=1` confirmed
  in commit f927298 to render the capacity one-liner under the
  source list. Documented in that commit's body.

Also corrected the docstring on query()'s `retrieval_keywords` to
reflect that keywords don't enter cache_key DIRECTLY but do change
context_root + conversation_hash via source selection — so the same
question with different keywords lands under different cache_keys
(legitimately, since the LLM saw different contexts).
2026-05-01 11:51:30 -04:00
31701ad524
cli: human render for query by default; --json for raw; ensure_ascii=False
Two things fox surfaced from a "who is pikachu?" run:

1. Output emitted "Pok\\u00e9mon" instead of "Pokémon" — json.dumps
   defaulted to ensure_ascii=True. Switched to ensure_ascii=False on
   every user-facing dump in cli.py (41 sites). The one canonical-JSON
   call (`separators=(",", ":")` for storing in providence_cache as
   a JSON column, not for hashing) was deliberately left alone.

2. The actual answer was buried under cache_key / context_root / per-
   source metadata / timings JSON. Default now renders human-readable:

       who is pikachu?
         HYBRID  1/2 verified  via quote  9.2s  (fresh)

       Pikachu is a species of Pokémon creatures...

       sources (3):
         [1] Pikachu — en.wikipedia.org/wiki/Pikachu  (002.db)
         [2] List_of_Pokémon — en.wikipedia.org/wiki/List_of_Pokémon
         ...

       unverified (1):
         - "spans the model couldn't ground..."

       cache_key: 35ab7d33…   <run with --json for full record>

   Pass `--json` to get the prior raw record (still ensure_ascii=False
   so unicode renders cleanly there too — scripts parsing the output
   see real chars; the JSON spec accepts either form).

Implementation:

- `_render_query_human(result, question)` — pure function, easy to
  unit-test, no I/O. Truncates unverified spans over 100 chars,
  omits empty sections, distinguishes cached vs fresh in the summary.
- `_strip_scheme` / `_short_path` — small helpers for source display.
- `--json` flag on the `query` subparser; default is human render.

Tests: 12 new in tests/test_cli_render.py — question position, summary
fields (audit/n_verified/method/elapsed/cache-status), unicode
literals (Pokémon not \\u00e9), source line shape, long-quote
truncation, empty-section omission, error fallback, short cache_key
with --json hint. 309 passed, 1 skipped overall.
2026-04-29 10:14:36 -04:00