Skip to content

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

Open
PRAteek-singHWY wants to merge 6 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 6 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_6

Conversation

@PRAteek-singHWY

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

Summary by CodeRabbit

  • New Features

    • Added confidence calibration for ranked candidates, including probability scoring and calibration-quality measurement.
    • Added automatic linking or human-review decisions based on confidence, safety checks, and candidate availability.
    • Expanded live evaluation to report calibration quality and decision accuracy.
  • Bug Fixes

    • Added validation for invalid confidence, threshold, temperature, and calibration inputs.
  • Documentation

    • Documented the calibration and decision-routing behavior.
  • Tests

    • Added comprehensive coverage for calibration, decision outcomes, boundary conditions, and safety scenarios.

Walkthrough

Changes

Adds temperature-scaling calibration and ECE measurement, introduces confidence-based auto-link/review routing, and extends the live librarian evaluation script with shared pipeline construction, calibration reporting, and decision-accuracy reporting.

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.
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.
Calibration test coverage
application/tests/librarian/temperature_test.py
Adds hermetic tests for probabilities, fitting, ECE calculations, validation, end-to-end confidence behavior, and metadata.
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: northdpole, paoga87, robvanderveer, 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 accurately summarizes the new C.4 decision engine and golden-set decision gate.
Description check ✅ Passed The description matches the changeset and explains the decision engine, evaluation harness, and tests.
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 on lines +1 to +13
"""Module C.3 — confidence calibration (Week 5).

C.2 (the cross-encoder, W4) emits a raw ranking logit per candidate — great for
ordering, meaningless as confidence (a +1.5 is not "82% sure"). C.3 turns that
logit into an honest probability via **temperature scaling**: ``p = sigmoid(z/T)``
with a single scalar ``T`` fit by negative-log-likelihood on the golden set, and
proves the result honest with **ECE < 0.10**.

The W6 decision engine thresholds that probability (auto-link vs. human review),
so calibration is what makes the threshold trustworthy. Kept dependency-light
(numpy + scipy) and model-free so it stays hermetically testable — mirrors the
C.1/C.2 seams.
"""

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring describes the rejected sigmoid approach, not the actual softmax-over-shortlist implementation.

This package docstring says calibration is p = sigmoid(z/T) on a single logit. But temperature.py's own module docstring explicitly explains why that single-logit sigmoid approach doesn't work and why it instead calibrates softmax(logits / T) over the whole shortlist. This file should describe the actual algorithm to avoid misleading readers.

📝 Suggested fix
-logit into an honest probability via **temperature scaling**: ``p = sigmoid(z/T)``
-with a single scalar ``T`` fit by negative-log-likelihood on the golden set, and
+logit into an honest probability via **temperature scaling**: calibrating the
+*softmax over the whole shortlist*, ``p = softmax(logits / T)``, with confidence
+being the top-1 mass — a single scalar ``T`` fit by negative-log-likelihood on
+the golden set, and
 proves the result honest with **ECE < 0.10**.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""Module C.3confidence calibration (Week 5).
C.2 (the cross-encoder, W4) emits a raw ranking logit per candidategreat for
ordering, meaningless as confidence (a +1.5 is not "82% sure"). C.3 turns that
logit into an honest probability via **temperature scaling**: ``p = sigmoid(z/T)``
with a single scalar ``T`` fit by negative-log-likelihood on the golden set, and
proves the result honest with **ECE < 0.10**.
The W6 decision engine thresholds that probability (auto-link vs. human review),
so calibration is what makes the threshold trustworthy. Kept dependency-light
(numpy + scipy) and model-free so it stays hermetically testablemirrors the
C.1/C.2 seams.
"""
"""Module C.3confidence calibration (Week 5).
C.2 (the cross-encoder, W4) emits a raw ranking logit per candidategreat for
ordering, meaningless as confidence (a +1.5 is not "82% sure"). C.3 turns that
logit into an honest probability via **temperature scaling**: calibrating the
*softmax over the whole shortlist*, ``p = softmax(logits / T)``, with confidence
being the top-1 massa single scalar ``T`` fit by negative-log-likelihood on
the golden set, and proves the result honest with **ECE < 0.10**.
The W6 decision engine thresholds that probability (auto-link vs. human review),
so calibration is what makes the threshold trustworthy. Kept dependency-light
(numpy + scipy) and model-free so it stays hermetically testablemirrors the
C.1/C.2 seams.
"""
🤖 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/__init__.py` around lines 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.

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