Skip to content

week_6b: Module C (The Librarian) — C.4 envelope emitter + C.0→C.4 pipeline glue#991

Open
PRAteek-singHWY wants to merge 8 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_6b
Open

week_6b: Module C (The Librarian) — C.4 envelope emitter + C.0→C.4 pipeline glue#991
PRAteek-singHWY wants to merge 8 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_6b

Conversation

@PRAteek-singHWY

Copy link
Copy Markdown
Contributor

Hi @northdpole — Week 6b of Module C, stacked on the Week-6 decision engine. Week 6 produced the verdict (auto-link vs. review); this PR turns that verdict into the wire envelopes Module D consumes, and wires the whole C.0→C.4 pipeline end to end (dry-run).

Stacked on gsocmodule_C_week_6 (the C.4 decision engine), which is itself on #974 (Week 5). Only the top two commits are new. I'll rebase the stack onto main as #974 → Week 6 land, shrinking the diff to the Week-6b-only surface (4 files). No dependency on Modules A or B: it runs on the golden fixture with injected stubs.

Overview

Week 6 gave us decide() → a DecisionResult. Two things were deliberately left out of that PR to keep it a clean, provable unit: emitting the RFC envelope, and wiring the stages together. This PR adds both.

This PR's role:

  1. The emitter (emitter.py)emit(section, audit, result, *, pipeline_run_id, at) dispatches on the verdict: linked → an RFC LinkProposal, review → an RFC ReviewItem. It only builds the envelope (persisting it is W8). Pure and timestamp-injected (no clock read), so every branch is hermetically testable. Auto-links carry link_type "Automatically linked to" (mirrors cre_defs.LinkTypes) and the calibrated confidence; reviews carry the reason_code and a deterministic review_id derived from the chunk id. update_detection defaults to the declared-degraded value (is_update=False) until the SafetyGuard lands.

  2. The pipeline (pipeline.py)LibrarianPipeline.run(at=...) runs C.0→C.4 over a knowledge source: section_from_queue_row (C.0) → retrieve (C.1) → rerank (C.2) → scaler.confidence (C.3) → decide + emit (C.4), one envelope per valid row. Every stage is an injected seam, so the whole pipeline runs hermetically with stubs. Inherently dry-run — builds envelopes, never persists. pipeline_run_id and the timestamp are injected, never read from the clock, so a run is reproducible.

Scope: 2 new modules + 2 new tests. No frontend, no migration, no behaviour change to OpenCRE proper.

What changed

Area Files Description
C.4 emitter emitter.py (new) emit() + build_link_proposal / build_review_item. DecisionResult → RFC LinkProposal / ReviewItem, snapshotting the chunk (KnowledgeSnapshot) and passing the C.1/C.2 RetrievalAudit through untouched. Pure, timestamp-injected, custom EmitterError; auto-link link_type mirrors cre_defs.LinkTypes.AutomaticallyLinkedTo; degraded update_detection default; deterministic review_id.
C.0→C.4 pipeline pipeline.py (new) LibrarianPipeline.run(at=...) -> RunResult(envelopes, RunStats). Wires all five stages via injected seams (source/retriever/reranker/scaler), inherently dry-run. Rows rejected at the C.0 boundary (e.g. UNCERTAIN) are skipped and counted, not linked.
Tests emitter_test.py (new), pipeline_test.py (new) 14 hermetic tests. Emitter (9): both envelopes end-to-end, audit passthrough, degraded update_detection, no-candidates review has no suggestions, the verdict/reason guards, and link_type == cre_defs single source of truth. Pipeline (5): confident row auto-links, low confidence reviews (below_threshold), empty shortlist reviews (no_candidates), UNCERTAIN row skipped at the boundary, and mixed-batch counts.

How the pieces connect

flowchart TB
    row["knowledge_queue row"]
    subgraph PIPE["LibrarianPipeline.run (this PR)"]
        c0["C.0 section_from_queue_row"]
        c1["C.1 retriever.retrieve"]
        c2["C.2 reranker.rerank"]
        c3["C.3 scaler.confidence"]
        c4["C.4 decide()"]
        emit["emit()"]
        c0 --> c1 --> c2 --> c3 --> c4 --> emit
    end
    row --> c0
    emit --> lp["LinkProposal (linked)"]
    emit --> ri["ReviewItem (review + reason_code)"]
    skip["UNCERTAIN / invalid row -> skipped, counted"]
    c0 -.-> skip
Loading

Results

# offline (CI default) — hermetic, no key/DB/model
141 librarian tests passing (127 from W1–W6 + 14 new; 1 skipped)

The emitter and pipeline are pure and dry-run — every branch is covered by the hermetic tests above, no live key required. The end-to-end demo on the golden set (populated envelopes for a full slice) is the midterm deliverable; this PR lands the machinery it runs on.

What is intentionally not here

  • SafetyGuard (ood / conformal / update_detector) that would populate the adversarial / update_ambiguous flags and real update_detection.
  • cre_main dispatch and persistence / queue write-back / graph writes (W8) — the pipeline stays dry-run; nothing is written to OpenCRE.
  • Live B→C integration (W8) — the pipeline reads via C's own KnowledgeQueueItem mirror over the golden fixture, not a live connection to Module B.

How to verify locally

# the new emitter + pipeline tests (hermetic — no key, DB, or model)
python3 -m unittest application.tests.librarian.emitter_test application.tests.librarian.pipeline_test
# or the whole librarian suite
python3 -m unittest discover -s application/tests/librarian -p '*_test.py' -t .

PRAteek-singHWY and others added 8 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.
…l / ReviewItem)

The decision engine (week_6) yields a verdict; this turns it into the wire contract
Module D consumes.

- emitter.py: `emit(section, audit, result, *, pipeline_run_id, at)` dispatches on
  the verdict — `linked` -> RFC LinkProposal, `review` -> RFC ReviewItem — plus the
  two explicit builders. Pure and timestamp-injected (no clock read) so it is
  hermetically testable; only builds the envelope (persistence is W8). Auto-links
  carry link_type "Automatically linked to" (mirrors cre_defs.LinkTypes) and the
  calibrated confidence; reviews carry the reason_code and a deterministic
  review_id derived from the chunk id. update_detection defaults to the declared
  degraded value (is_update=False) until the SafetyGuard lands.
- emitter_test.py: 9 hermetic tests — both envelopes end-to-end, audit passthrough,
  degraded update_detection, no-candidates review has no suggestions, the
  verdict/reason guards, and link_type == cre_defs single source of truth.

Stacked on week_6. Pipeline glue (C.0->C.4) follows next.
Wires the librarian end to end: section_from_queue_row (C.0) -> retriever (C.1) ->
reranker (C.2) -> scaler.confidence (C.3) -> decide + emit (C.4), one envelope per
valid knowledge_queue row.

- pipeline.py: LibrarianPipeline.run(at=...) -> RunResult(envelopes, RunStats).
  Every stage is an injected seam (source/retriever/reranker/scaler), so the whole
  pipeline runs hermetically with stubs. Inherently dry-run — builds envelopes,
  never persists (queue write-back + graph writes are W8). pipeline_run_id and the
  timestamp are injected, never read from the clock, so a run is reproducible. Rows
  rejected at the C.0 boundary (e.g. UNCERTAIN) are skipped and counted, not linked.
- pipeline_test.py: 5 hermetic tests — confident row auto-links, low confidence
  reviews (below_threshold), empty shortlist reviews (no_candidates), UNCERTAIN row
  skipped at the boundary, and mixed-batch counts.

Stacked on the week_6b emitter.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added confidence calibration using temperature scaling to improve probability estimates and threshold decisions.
    • Added automated decision routing that creates link proposals for high-confidence matches and review items for uncertain or flagged content.
    • Added a dry-run librarian pipeline with processing statistics for linked, reviewed, and skipped items.
    • Added calibration and decision-accuracy reporting to live evaluation runs.
  • Documentation

    • Clarified the librarian workflow and calibration stage.

Walkthrough

Adds temperature-scaling calibration, deterministic decision and envelope generation, an injected librarian pipeline, live calibration and decision evaluation reports, and hermetic tests covering the new behavior.

Changes

Librarian calibration and decision pipeline

Layer / File(s) Summary
Temperature calibration
application/utils/librarian/calibration/*, application/tests/librarian/temperature_test.py, application/utils/librarian/__init__.py
Adds temperature-scaled probabilities, temperature fitting, NLL, ECE validation, calibration metadata, documentation, and comprehensive tests.
Decision and envelope emission
application/utils/librarian/decision_engine.py, application/utils/librarian/emitter.py, application/tests/librarian/decision_engine_test.py, application/tests/librarian/emitter_test.py
Adds validated decision results, reason-code precedence, link/review envelope builders, deterministic review identifiers, and focused tests.
Pipeline orchestration
application/utils/librarian/pipeline.py, application/tests/librarian/pipeline_test.py
Adds injected retrieval, reranking, calibration, decision, emission, invalid-row skipping, and run statistics.
Live evaluation integration
scripts/evaluate_librarian.py
Reuses one live retrieval/reranking stack across recall, calibration, and decision reports, and returns calibration gate status.
Estimated code review effort: 4 (Complex) ~45 minutes

Possibly related PRs

  • OWASP/OpenCRE#957: Shares the live librarian retrieval and reranking evaluation flow refactoring.

Suggested reviewers: paoga87, northdpole, pa04rth

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.00% 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 names the new emitter and dry-run C.0→C.4 pipeline glue, matching the main changes.
Description check ✅ Passed The description clearly explains the emitter, pipeline wiring, tests, and out-of-scope items, so it matches the changeset.
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 (3)
application/utils/librarian/pipeline.py (2)

58-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Constructor params source/retriever/reranker/scaler are untyped.

Unlike threshold: float / pipeline_run_id: str in the same signature, and unlike the fully-typed decision_engine.py/emitter.py, these duck-typed seams carry no type hints at all. Consider Protocol classes (e.g. RetrieverLike, RerankerLike, ScalerLike) or at minimum Any annotations for make mypy consistency.

As per coding guidelines, "Run make mypy for Python type checking."

🤖 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/pipeline.py` around lines 58 - 73, Update the
constructor in the pipeline class around __init__ to annotate source, retriever,
reranker, and scaler, preferably using appropriate Protocol types such as
RetrieverLike, RerankerLike, and ScalerLike; use Any only where no suitable
interface exists. Preserve the existing threshold and pipeline_run_id
annotations and run make mypy to verify the changes.

Source: Coding guidelines


75-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Per-row failures in retrieve/rerank/confidence/decide/emit abort the whole run.

Only section_from_queue_row is guarded with try/except; a single failure from any other stage propagates and aborts the entire batch, discarding all envelopes/stats accumulated so far. This is currently safe with hermetic stubs, but the docstring states these seams are meant to become live DB/embedding/cross-encoder calls — worth hardening before that wiring lands.

♻️ Suggested per-row error containment
-            audit = self._retriever.retrieve(section.text)
-            audit = self._reranker.rerank(section.text, audit)
-            reranked = [c for c in audit.reranked if c.score_rerank is not None]
-            logits = [float(c.score_rerank) for c in reranked]
-            cre_ids = [c.cre_id for c in reranked]
-            confidence = self._scaler.confidence(logits) if logits else 0.0
-
-            result = decide(confidence, cre_ids, threshold=self._threshold)
-            envelope = emit(section, audit, result, pipeline_run_id=self._run_id, at=at)
-            envelopes.append(envelope)
-            if isinstance(envelope, LinkProposal):
-                linked += 1
-            else:
-                review += 1
+            try:
+                audit = self._retriever.retrieve(section.text)
+                audit = self._reranker.rerank(section.text, audit)
+                reranked = [c for c in audit.reranked if c.score_rerank is not None]
+                logits = [float(c.score_rerank) for c in reranked]
+                cre_ids = [c.cre_id for c in reranked]
+                confidence = self._scaler.confidence(logits) if logits else 0.0
+                result = decide(confidence, cre_ids, threshold=self._threshold)
+                envelope = emit(section, audit, result, pipeline_run_id=self._run_id, at=at)
+            except Exception:
+                errored += 1  # new RunStats field
+                continue
+            envelopes.append(envelope)
+            if isinstance(envelope, LinkProposal):
+                linked += 1
+            else:
+                review += 1
🤖 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/pipeline.py` around lines 75 - 104, Update the
per-item processing in run so failures from retrieve, rerank, confidence,
decide, or emit are contained to that row instead of aborting the batch. Wrap
the full processing pipeline after section_from_queue_row in a per-row
try/except, increment the appropriate skipped/error statistic for failed rows,
and continue processing later items while preserving already-created envelopes
and existing successful-row counts.
scripts/evaluate_librarian.py (1)

288-307: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

report_decision_accuracy re-derives the calibration set and re-fits T, duplicating report_calibration's work.

Both functions build the identical positive+hard_negative (shortlist, label) set and call fit_temperature on it (lines 234-253 in report_calibration, lines 293-307 here). Since main() calls both sequentially over the same retriever/reranker, this doubles the live per-row reranker.rerank() (cross-encoder inference) cost with no behavioral difference — the fitted T will be identical.

Consider having report_calibration return (status, scaler) and passing the scaler into report_decision_accuracy, or extracting the calibration-set-building + fit into one shared helper called once from main().

🤖 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 288 - 307, The calibration data
and temperature scaler are redundantly recomputed in report_decision_accuracy
after report_calibration. Refactor report_calibration and main so calibration
fitting occurs once, returns its status and fitted scaler, and passes that
scaler into report_decision_accuracy; remove the duplicate cal_rows
construction, reranker.rerank calls, label validation, and fit_temperature
invocation while preserving existing skip/status behavior.
🤖 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/emitter.py`:
- Around line 52-62: Update _proposed_links so entries used as
ReviewItem.suggested_links are not labeled with AUTO_LINK_TYPE; use the
established distinct suggested-review link type if available, or omit link_type
when constructing ProposedLink. Preserve the existing confidence, rationale, and
CRE ID values.

---

Nitpick comments:
In `@application/utils/librarian/pipeline.py`:
- Around line 58-73: Update the constructor in the pipeline class around
__init__ to annotate source, retriever, reranker, and scaler, preferably using
appropriate Protocol types such as RetrieverLike, RerankerLike, and ScalerLike;
use Any only where no suitable interface exists. Preserve the existing threshold
and pipeline_run_id annotations and run make mypy to verify the changes.
- Around line 75-104: Update the per-item processing in run so failures from
retrieve, rerank, confidence, decide, or emit are contained to that row instead
of aborting the batch. Wrap the full processing pipeline after
section_from_queue_row in a per-row try/except, increment the appropriate
skipped/error statistic for failed rows, and continue processing later items
while preserving already-created envelopes and existing successful-row counts.

In `@scripts/evaluate_librarian.py`:
- Around line 288-307: The calibration data and temperature scaler are
redundantly recomputed in report_decision_accuracy after report_calibration.
Refactor report_calibration and main so calibration fitting occurs once, returns
its status and fitted scaler, and passes that scaler into
report_decision_accuracy; remove the duplicate cal_rows construction,
reranker.rerank calls, label validation, and fit_temperature invocation while
preserving existing skip/status behavior.
🪄 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: 7118b7e4-fc18-4e63-bd3f-3ab1008a3300

📥 Commits

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

📒 Files selected for processing (11)
  • application/tests/librarian/decision_engine_test.py
  • application/tests/librarian/emitter_test.py
  • application/tests/librarian/pipeline_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
  • application/utils/librarian/emitter.py
  • application/utils/librarian/pipeline.py
  • scripts/evaluate_librarian.py

Comment on lines +52 to +62
def _proposed_links(result: DecisionResult) -> list:
"""One ProposedLink per chosen CRE (top-1), carrying the calibrated confidence."""
return [
ProposedLink(
cre_id=cre_id,
link_type=AUTO_LINK_TYPE,
confidence=result.confidence,
rationale=None,
)
for cre_id in result.cre_ids
]

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the ProposedLink / suggested_links contract in the RFC schema and pydantic model.
rg -n "class ProposedLink" -A 15 application/utils/librarian/schemas.py
fd . application/utils/librarian/_rfc_schemas -e json | xargs -I{} sh -c 'echo "== {} =="; cat {}'

Repository: OWASP/OpenCRE

Length of output: 9981


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## emitter.py relevant sections"
sed -n '1,180p' application/utils/librarian/emitter.py

echo
echo "## schema definitions and constants"
fd -e py . application -x sh -c 'grep -n "LinkTypes|link_type|AUTO_LINK|ProposedLink|suggested_links|build_review_item|build_link_proposal" "$1" || true' sh {}

Repository: OWASP/OpenCRE

Length of output: 5152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## cre_defs link type definitions"
rg -n "class LinkTypes|AutomaticallyLinkedTo|Suggested|Suggested|Proposed.*Link|link_type" -S application -g '*.py' | head -200

echo
echo "## all references to suggested_links and proposed link types"
rg -n "suggested_links|ProposedLink\\(|link_type\\s*=|LinkTypes\\.Autom|Auto.*Link" -S application tests 2>/dev/null || true

echo
echo "## tests around emitted review/link proposal"
fd -e py . application tests -x sh -c 'grep -n "build_review_item\\|build_link_proposal\\|LinkProposal|ReviewItem|suggested_links\\[" "$1" || true' sh {}

Repository: OWASP/OpenCRE

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## focused cre_defs.py/link type refs"
fd -e py cre_defs application cre_ -x sh -c 'grep -n -C 4 "AutomaticallyLinkedTo\\|LinkTypes\\|Automatically.*Link\\|Suggested" "$1" || true' sh {}

echo
echo "## focused tests for emitted envelope and suggested link type"
fd -e py . tests test_ -x sh -c 'grep -n -C 5 "suggested_links\\|build_review_item\\|build_link_proposal\\|link_type\\|Automatically linked to" "$1" || true' sh {}

Repository: OWASP/OpenCRE

Length of output: 2218


Avoid labeling unlinked review suggestions as auto-links.

_proposed_links always emits link_type="Automatically linked to", but build_review_item uses it for ReviewItem.suggested_links, which are candidate links pending human review. Add a distinct suggested-review link type or omit link_type for these entries; otherwise consumers see an established auto-link where only a suggestion exists.

🤖 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/emitter.py` around lines 52 - 62, Update
_proposed_links so entries used as ReviewItem.suggested_links are not labeled
with AUTO_LINK_TYPE; use the established distinct suggested-review link type if
available, or omit link_type when constructing ProposedLink. Preserve the
existing confidence, rationale, and CRE ID values.

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