Skip to content

week_6: Module C (The Librarian) — C.4 decision engine + golden-set decision gate#990

Open
PRAteek-singHWY wants to merge 7 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_6
Open

week_6: Module C (The Librarian) — C.4 decision engine + golden-set decision gate#990
PRAteek-singHWY wants to merge 7 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_6

Conversation

@PRAteek-singHWY

@PRAteek-singHWY PRAteek-singHWY commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Hi @northdpole - Week 6 of Module C. Week 5 produced one honest confidence per chunk; this PR turns that number into the actual decision - auto-link into the graph, or route to a human - which is the accuracy gate of the whole pipeline.

Stacked on #974 (Week 5). Based on gsocmodule_C_week_5; only the top commit is new. Until #974 merges the diff shows the W5 commits too - I'll rebase onto main as it lands, shrinking it to the Week-6-only surface (3 files). No dependency on Modules A or B: this runs entirely on the golden dataset.

Overview

Week 3 built the search step (C.1), Week 4 the rerank step (C.2), and Week 5 the calibration step (C.3) - a single scalar T that maps the reranked shortlist to a trustworthy confidence = softmax(logits / T).

The problem: a calibrated confidence is only useful if something acts on it. Auto-linking a wrong CRE pollutes the graph; sending everything to a human defeats the point. We need a rule that auto-links when it's safe and escalates when it isn't.

This PR's role: build the decision step (C.4) - decision_engine.decide(). It links the top-1 candidate iff confidence >= threshold and there is a candidate and no blocking safety flag; otherwise it routes to review with a reason_code. It's a pure function of (confidence, candidates, flags, threshold) -> DecisionResult - it does not import the C.3 scaler (confidence-in → decision-out), so it stays model-free and hermetically testable, mirroring the C.1/C.2/C.3 seams. Reason-code precedence is total: NO_CANDIDATES > ADVERSARIAL_FLAG > UPDATE_AMBIGUOUS > BELOW_THRESHOLD.

The harness gains a decision-accuracy gate: run the live C.1→C.4 decision over the golden set and measure how often decide() lands on the expected auto-link-vs-review call.

Scope: 1 new module + 1 new test + additive harness wiring. The SafetyGuard flags (adversarial / update_ambiguous) are accepted by decide() but not yet produced - nothing sets them, so they default to False (declared-degraded until that lands). No frontend, no migration, no behaviour change to OpenCRE proper.

What changed

Area Files Description
C.4 decision engine decision_engine.py (new) decide(confidence, candidate_cre_ids, *, threshold, adversarial, update_ambiguous) -> DecisionResult. Links the top-1 iff confidence >= threshold AND candidates exist AND no blocking flag; else reviews with a reason_code. Frozen DecisionResult, versioned ENGINE_NAME, custom DecisionError, input guards on confidence/threshold. Model-free and confidence-agnostic so it is hermetically testable - mirrors the C.1 embed_fn / C.2 score_fn / C.3 scaler seams.
Eval harness evaluate_librarian.py additive report_decision_accuracy: fits T on positive+hard_negative, runs the live C.1→C.4 decision over the golden set, and reports overall agreement plus auto-link recall vs review recall (a single accuracy hides that at tau=0.80 the softmax top-1 mass of a correct-but-close winner is often ~0.5, so correct positives route to review - the safe direction). Informational, not a hard gate: tuning tau is the Week-7 experiment, and flag-based reason codes lag until the SafetyGuard lands. report_calibration (W5) untouched.
Tests decision_engine_test.py (new) 14 hermetic tests - table-driven over every confidence/flag combination, the inclusive >= boundary, all four reason codes, the precedence order, and the input guards.

How the pieces connect

flowchart TB
    conf["C.3 calibrated confidence<br/>+ reranked candidates + flags"]
    subgraph C4["C.4 - decision engine (this PR)"]
        rule["decide(): confidence at or above tau ?<br/>AND candidates exist AND no blocking flag"]
        res["DecisionResult<br/>(decision, confidence, cre_ids, reason_code)"]
        rule --> res
    end
    conf --> rule
    res --> link["linked -> LinkProposal (W6b emits)"]
    res --> review["review -> ReviewItem + reason_code<br/>NO_CANDIDATES / ADVERSARIAL_FLAG /<br/>UPDATE_AMBIGUOUS / BELOW_THRESHOLD"]
Loading

Results

# offline (CI default) - hermetic, no key/DB/model
127 librarian tests passing (113 from W1–W5 + 14 new; 1 skipped)
explicit slice (C.0.5 resolver): 5/5 - gate 100%: PASS

# live (local, against the migrated embedding_vec cache: 428 CRE hub vectors,
# gemini/gemini-embedding-001 dim 3072, ms-marco-MiniLM-L-6-v2, hub-firewall ON)
retrieval recall@20 (C.1): any-hit 285/292 (98%)
rerank top-1     (C.2): 220/292 (75%)
calibration (C.3, 304 rows): T=1.105; ECE 0.046 (calibrated); gate ECE<0.10: PASS
decision (C.4, 319 rows @ tau=0.80): overall 181/319 (57%)
  auto-link recall (expected-linked): 176/314 (56%)
  review recall   (expected-review): 5/5 (100%); reason_code 4/5 (80%)

Reading the C.4 line by direction, because a single accuracy hides the story:

  • Review recall 5/5 (100%) - every chunk that should go to a human does. The engine never wrongly auto-links something that needs review. For an accuracy gate, this is the number that matters most.
  • Auto-link recall 56% - at tau=0.80 many correct-but-close positives fall below the bar and route to review (the safe direction). This is a conservative starting point; Week 7's threshold sweep is exactly the lever that lifts it.
  • reason_code 4/5 - the one miss is a flag-based code that needs the SafetyGuard (not wired; declared).

What is intentionally not here

  • Envelope emitters + pipeline glue (W6b) - turning a DecisionResult into the RFC LinkProposal / ReviewItem and wiring C.0→C.4. Ships stacked as week_6b.
  • SafetyGuard flags (ood / conformal / update-detection) that would populate adversarial / update_ambiguous.
  • Persisting the fitted T for the live decision path, and live B→C integration + graph writes (W8) - the pipeline stays dry-run.

How to verify locally

# the new decision-engine tests (hermetic - no key, DB, or model)
python3 -m unittest application.tests.librarian.decision_engine_test
# or the whole librarian suite
python3 -m unittest discover -s application/tests/librarian -p '*_test.py' -t .

# live decision gate (needs a populated cache DB + an embedding-capable LLM).
# after the pgvector migration (#979), migrate a legacy SQLite cache first:
python3 scripts/rewrite_sqlite_embeddings_to_vec.py --db standards_cache.sqlite
python3 scripts/evaluate_librarian.py \
    --dataset application/tests/librarian/fixtures/golden_dataset.json \
    --use_live_embeddings --cache_file standards_cache.sqlite

PRAteek-singHWY and others added 6 commits July 9, 2026 22:54
… once

report_retrieval_recall and report_calibration each built the live
pipeline (DB + embedding model + cross-encoder) independently, loading it
twice and reranking every positive row twice per --use_live_embeddings run.
Build it once in main and pass (retriever, reranker) into both reports,
matching _build_live_pipeline's stated intent. Behavior-preserving:
recall@20 285/292, rerank top-1 220/292, ECE 0.046 PASS unchanged.
…ecision gate

C.3 (Week 5) produces one honest, calibrated confidence per chunk; C.4 turns it
into the action — auto-link into the OpenCRE graph, or route to a human — which is
the accuracy gate of the whole pipeline.

- decision_engine.py: pure `decide(confidence, candidates, *, threshold,
  adversarial, update_ambiguous) -> DecisionResult`. Links the top-1 iff
  confidence >= threshold AND candidates exist AND no blocking flag; otherwise
  reviews with a reason_code. Reason precedence NO_CANDIDATES > ADVERSARIAL_FLAG >
  UPDATE_AMBIGUOUS > BELOW_THRESHOLD. Frozen result, versioned ENGINE_NAME, custom
  DecisionError — mirrors the C.1/C.2/C.3 model-free seams. Does not import the C.3
  scaler (confidence-in -> decision-out), so it is hermetically testable.
- decision_engine_test.py: 14 hermetic tests — table-driven over every
  confidence/flag combination, the inclusive >= boundary, all four reason codes,
  precedence order, and the input guards.
- evaluate_librarian.py: additive report_decision_accuracy — fits T on
  positive+hard_negative, runs the live C.1->C.4 decision over the golden set, and
  reports overall agreement plus auto-link recall vs review recall (a single
  accuracy hides that at tau=0.80 the softmax top-1 mass of a correct-but-close
  winner is often ~0.5, so correct positives route to review — the safe direction;
  W7 tunes tau). Informational, not a gate: the SafetyGuard flags are not wired
  yet, so flag-based reason codes lag until that lands.

Emitters (LinkProposal/ReviewItem writers) and the C.0->C.4 pipeline glue follow
in a stacked week_6b PR.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 97f6055d-a3f8-4bb7-8ce3-7453adfe6aaf

📥 Commits

Reviewing files that changed from the base of the PR and between fccfaab and 001bd6e.

📒 Files selected for processing (1)
  • application/utils/librarian/calibration/__init__.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • application/utils/librarian/calibration/init.py

Summary by CodeRabbit

  • New Features

    • Added confidence calibration that converts ranking results into calibrated probabilities.
    • Added automatic routing to link high-confidence matches or send uncertain and flagged cases for review.
    • Added live evaluation reports for calibration quality and decision accuracy.
    • Live evaluations now reuse the retrieval and ranking pipeline for improved efficiency.
  • Tests

    • Added comprehensive coverage for calibration, decision thresholds, validation, safety flags, and evaluation behavior.
  • Documentation

    • Updated librarian module documentation to describe calibration and decision routing.

Walkthrough

Adds temperature-scaling calibration, confidence-based link/review decisions, hermetic tests, and live evaluation reporting with shared retrieval and reranking components.

Changes

Librarian calibration and routing

Layer / File(s) Summary
Temperature calibration API and validation
application/utils/librarian/calibration/..., application/utils/librarian/__init__.py
Adds temperature scaling, NLL fitting, ECE calculation, validation errors, version metadata, and calibration documentation.
Calibration test coverage
application/tests/librarian/temperature_test.py
Adds hermetic tests for probabilities, fitting, ECE calculations, validation, confidence behavior, and metadata.
Decision routing contract and tests
application/utils/librarian/decision_engine.py, application/tests/librarian/decision_engine_test.py
Adds immutable decision results, input guards, precedence rules, and tests for link/review outcomes and reason codes.
Live evaluation orchestration
scripts/evaluate_librarian.py
Builds the live pipeline once, reuses it across reports, evaluates calibration and decision accuracy, and returns evaluation status from main().
Estimated code review effort: 4 (Complex) ~45 minutes

Possibly related PRs

Suggested reviewers: robvanderveer, northdpole, paoga87, pa04rth

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: the C.4 decision engine and golden-set decision gate.
Description check ✅ Passed The description is directly related to the changeset and accurately explains the decision engine and evaluation harness updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
application/utils/librarian/calibration/temperature.py (1)

63-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor duplication between _softmax_top and probabilities.

Both implement the same "asarray + empty check + softmax" logic independently. confidence() could derive from probabilities() instead of a separate module-level helper, keeping the empty-shortlist guard in one place.

♻️ Suggested consolidation
     def confidence(self, logits: Sequence[float]) -> float:
         """P(the top candidate is correct) — the top-1 mass of the softmax.

         This is the number the W6 decision engine thresholds on.
         """
-        return _softmax_top(logits, self.temperature)
+        return float(self.probabilities(logits).max())

Also applies to: 105-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/utils/librarian/calibration/temperature.py` around lines 63 - 68,
Consolidate the duplicated shortlist conversion, empty-check, and softmax logic
by removing or bypassing `_softmax_top` and deriving `confidence()` from the
existing `probabilities()` implementation. Ensure `probabilities()` remains the
single guard for empty candidate shortlists while preserving the top-1
probability result and temperature behavior.
scripts/evaluate_librarian.py (1)

234-244: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Live retrieve+rerank (and the temperature fit) is redundantly recomputed 2-3x per row.

report_decision_accuracy rebuilds the exact same cal_rows set and reruns retriever.retrieve/reranker.rerank per row to refit a second TemperatureScaler, duplicating work report_calibration (called right before it in main, L458-459) already did. Then its own graded loop reruns retrieve/rerank again for rows that overlap with cal_rows (e.g. positive-slice rows with an expected decision). Since retrieval/reranking against a live embedding model + cross-encoder is the expensive part this harness gates behind --use_live_embeddings, this triples model calls for no functional benefit — the fit and audits are deterministic given the same inputs.

Consider having report_calibration return the fitted TemperatureScaler (and/or the per-row audits) so report_decision_accuracy reuses them instead of recomputing, and caching each row's retrieve+rerank result by row id so the cal_rows/graded loops don't redo live calls for the same row.

Also applies to: 292-333

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/evaluate_librarian.py` around lines 234 - 244, Refactor
report_calibration and report_decision_accuracy to reuse the fitted
TemperatureScaler and per-row rerank audits instead of rerunning retrieve and
rerank. Have report_calibration return the scaler and/or cached audits, pass
them from main into report_decision_accuracy, and ensure overlapping cal_rows
and graded rows retrieve each row only once, keyed by a stable row identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@application/utils/librarian/calibration/__init__.py`:
- Around line 1-13: Update the package docstring in the module-level
documentation to describe the implemented shortlist-wide softmax calibration
used by temperature.py, replacing the single-logit sigmoid formula and related
claims. Explain that logits are scaled by a fitted scalar temperature and
normalized across each candidate shortlist, while preserving the existing
purpose, NLL fitting, and ECE context.

---

Nitpick comments:
In `@application/utils/librarian/calibration/temperature.py`:
- Around line 63-68: Consolidate the duplicated shortlist conversion,
empty-check, and softmax logic by removing or bypassing `_softmax_top` and
deriving `confidence()` from the existing `probabilities()` implementation.
Ensure `probabilities()` remains the single guard for empty candidate shortlists
while preserving the top-1 probability result and temperature behavior.

In `@scripts/evaluate_librarian.py`:
- Around line 234-244: Refactor report_calibration and report_decision_accuracy
to reuse the fitted TemperatureScaler and per-row rerank audits instead of
rerunning retrieve and rerank. Have report_calibration return the scaler and/or
cached audits, pass them from main into report_decision_accuracy, and ensure
overlapping cal_rows and graded rows retrieve each row only once, keyed by a
stable row identifier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c77096d-5a49-4d8f-9eee-e6a67592cb12

📥 Commits

Reviewing files that changed from the base of the PR and between a55e380 and fccfaab.

📒 Files selected for processing (7)
  • application/tests/librarian/decision_engine_test.py
  • application/tests/librarian/temperature_test.py
  • application/utils/librarian/__init__.py
  • application/utils/librarian/calibration/__init__.py
  • application/utils/librarian/calibration/temperature.py
  • application/utils/librarian/decision_engine.py
  • scripts/evaluate_librarian.py

Comment thread application/utils/librarian/calibration/__init__.py
…string

The calibration/__init__.py docstring still described the rejected single-logit
`p = sigmoid(z/T)` approach. temperature.py actually calibrates the softmax over
the whole shortlist (`p = softmax(logits / T)`, confidence = top-1 mass) — the
sigmoid-on-one-logit approach cannot be calibrated by temperature alone. Docstring
now matches the implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant