Skip to content

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

Merged
northdpole merged 5 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_6b
Aug 9, 2026
Merged

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

Conversation

@PRAteek-singHWY

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

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 .

@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: e278f583-7482-4bfe-9a50-86af9637486f

📥 Commits

Reviewing files that changed from the base of the PR and between 332db47 and 5f2d75b.

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

Summary by CodeRabbit

  • New Features

    • Added a dry-run knowledge-processing pipeline with retrieval, reranking, confidence calibration, and decision routing.
    • Automatically creates link proposals for high-confidence matches and review items for uncertain, ambiguous, flagged, or candidate-free entries.
    • Added deterministic review suggestions, timestamps, confidence details, and processing statistics.
    • Enhanced evaluation reporting with shared calibration results and decision-accuracy metrics.
  • Bug Fixes

    • Improved per-item error handling so failures are recorded without interrupting successful processing.
  • Documentation

    • Documented confidence calibration and the end-to-end decision-routing workflow.

Walkthrough

The PR adds a deterministic decision engine, envelope emitter, and dry-run librarian pipeline. It integrates shared calibration into live evaluation reports and adds hermetic tests for these flows.

Changes

Librarian decision and envelope flow

Layer / File(s) Summary
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
The decision engine validates inputs, applies reason precedence, and returns frozen results. The emitter creates validated link and review envelopes.
Pipeline orchestration
application/utils/librarian/pipeline.py, application/tests/librarian/pipeline_test.py, application/utils/librarian/__init__.py
LibrarianPipeline processes rows through retrieval, reranking, calibration, decisioning, and emission. It records outcomes and contains per-row stage errors.
Live calibration and decision reporting
scripts/evaluate_librarian.py, application/utils/librarian/calibration/__init__.py, application/tests/librarian/evaluate_harness_test.py, application/tests/librarian/dataset_test.py
The evaluation harness shares audits, builds calibration data, returns the fitted scaler, and runs decision accuracy only when calibration succeeds. Harness loading now reports missing import metadata explicitly.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • OWASP/OpenCRE#922: Extends the librarian contracts and evaluation harness introduced there.
  • OWASP/OpenCRE#990: Overlaps with the decision engine and evaluation harness changes.
  • OWASP/OpenCRE#974: Provides the temperature-scaled confidence consumed by decision routing.

Suggested reviewers: pa04rth, paoga87, robvanderveer

🚥 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 clearly summarizes the PR's main changes: the C.4 envelope emitter and the C.0→C.4 pipeline glue.
Description check ✅ Passed The description directly explains the emitter, pipeline, dry-run scope, exclusions, tests, and verification steps.
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 thread application/utils/librarian/emitter.py Outdated
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Jul 24, 2026
…ions as auto-links

_proposed_links stamped link_type "Automatically linked to" on every ProposedLink,
including a ReviewItem's suggested_links — but those are candidates for a human to
consider, not auto-links. Parametrize the link type: LinkProposal.links use
AUTO_LINK_TYPE, ReviewItem.suggested_links use SUGGESTED_LINK_TYPE ("Related",
mirrors cre_defs.LinkTypes.Related). Test asserts a review suggestion is never
labelled as an auto-link.

@northdpole northdpole left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer review — Module C Week 6b (#991)

Emitter + dry-run pipeline glue look good: type guards, Related vs Automatically linked to, injected at / pipeline_run_id, and hermetic tests are in good shape. No blocking bugs in the unique Week-6b surface.

Stacked on #990 / #974 — rebase as those land. Inline notes are non-blocking for this dry-run PR but matter before W8 persistence.

Comment thread application/utils/librarian/pipeline.py Outdated
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note (pre-W8) — SafetyGuard flags not wired

decide(...) is called without adversarial= / update_ambiguous=, so those reason codes can never fire from this pipeline yet. Fine for dry-run Week 6b, but must be wired before any graph / queue write-back (W8), or auto-links will ignore the safety path the decision engine already supports.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and noted as a W8 blocker rather than a change here. decide(...) gets adversarial= / update_ambiguous= wired in before any graph or queue write-back, so the safety reason codes can actually fire. Leaving it unwired in this dry-run PR since there is no SafetyGuard to feed them yet.

for item in self._source.items():
total += 1
try:
section = section_from_queue_row(item)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Note (B↔C integration) — queue row shape still the C mirror

section_from_queue_row / C's KnowledgeQueueItem still expect the flat source_repo / source_path / source_commit_sha mirror. Module B's live knowledge_queue (#989) is a richer row (locator_*, content_hash, provenance columns, …). Already called out in section_validator for W8 — please keep this on the W8 checklist so the dry-run pipeline here does not silently assume the wrong row shape when wired to Postgres.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept on the W8 checklist. Reconciling section_from_queue_row / KnowledgeQueueItem against Module B's live knowledge_queue (#989) is a real schema reconciliation, not a rename: B's row carries locator_*, content_hash, and the provenance columns, where C currently assumes the flat source_repo / source_path / source_commit_sha mirror. The dry-run pipeline here stays on the mirror shape deliberately, and the adapter seam is where the mapping lands once B freezes the table.

PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 4, 2026
…he live reports

report_decision_accuracy rebuilt the positive + hard_negative calibration set
from its own retrieve+rerank pass and fit its own temperature, duplicating what
report_calibration had already done a few lines earlier. On a live run that meant
a third pipeline pass over the calibration slices and two independent fits of the
same T, with nothing guaranteeing the two agreed.

Now there is one of each:

- calibration_set() extracts the (shortlist, label) derivation, so the C.3 gate
  and the C.4 report read the same pairs off the same shared audits.
- report_calibration returns (status, scaler); report_decision_accuracy takes the
  fitted scaler instead of fitting its own, so C.4 thresholds on exactly the T
  the ECE gate measured.
- A degenerate set yields (1, None) and C.4 is skipped with a message: no fitted
  T means no honest confidence to threshold, and the run has already failed.
- The live audit set now covers expected-decision rows too. C.4 grades those and
  they are not confined to the positive/hard_negative slices, so keying them off
  the calibration slices alone would have dropped them.

Adds evaluate_harness_test.py. The live reports only run under
--use_live_embeddings, so nothing exercised their wiring — which is exactly the
code that has to share one pipeline pass and one T across three reports. A
counting stub asserts the pipeline is called once per row and not once per
report, that only the two calibration slices enter the fit, and that a degenerate
set returns status 1 with no scaler rather than reporting success. Verified the
last one fails if the gate is flipped back to 0.

134 librarian tests pass; the hermetic harness run still exits 0.
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 4, 2026
…he live reports

report_decision_accuracy rebuilt the positive + hard_negative calibration set
from its own retrieve+rerank pass and fit its own temperature, duplicating what
report_calibration had already done a few lines earlier. On a live run that meant
a third pipeline pass over the calibration slices and two independent fits of the
same T, with nothing guaranteeing the two agreed.

Now there is one of each:

- calibration_set() extracts the (shortlist, label) derivation, so the C.3 gate
  and the C.4 report read the same pairs off the same shared audits.
- report_calibration returns (status, scaler); report_decision_accuracy takes the
  fitted scaler instead of fitting its own, so C.4 thresholds on exactly the T
  the ECE gate measured.
- A degenerate set yields (1, None) and C.4 is skipped with a message: no fitted
  T means no honest confidence to threshold, and the run has already failed.
- The live audit set now covers expected-decision rows too. C.4 grades those and
  they are not confined to the positive/hard_negative slices, so keying them off
  the calibration slices alone would have dropped them.

Adds evaluate_harness_test.py. The live reports only run under
--use_live_embeddings, so nothing exercised their wiring — which is exactly the
code that has to share one pipeline pass and one T across three reports. A
counting stub asserts the pipeline is called once per row and not once per
report, that only the two calibration slices enter the fit, and that a degenerate
set returns status 1 with no scaler rather than reporting success. Verified the
last one fails if the gate is flipped back to 0.

134 librarian tests pass; the hermetic harness run still exits 0.

(cherry picked from commit 6ef7865)
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 4, 2026
…in per-row failures

Two nitpicks on pipeline.py, both aimed at the wiring rather than the dry-run
behaviour:

- The injected seams were the only untyped parameters in the signature, next to
  an annotated threshold/pipeline_run_id and fully-typed decision_engine/emitter.
  Each is structurally one method, so they are now Protocols — KnowledgeSource,
  Retriever, Reranker, Scaler — which keeps stubs as trivial as before while the
  call sites are checked. pipeline.py is clean under the --strict mypy the coding
  guidelines ask for; KnowledgeSource.items() is typed to what
  section_from_queue_row actually accepts, not a looser Mapping.

- Only the C.0 boundary was guarded, so a failure from retrieve / rerank /
  confidence / decide / emit propagated and abandoned the whole batch along with
  every envelope already built. Those stages are stubs today but become live DB,
  embedding, and cross-encoder calls in W8, where one timeout must not cost the
  run. Failures are now contained per row: the row is logged with its chunk and
  artifact id, counted in a new RunStats.errored, and the run continues.

errored is separate from skipped on purpose — a boundary rejection is expected
input hygiene, an error is a fault worth chasing. It defaults to 0, so existing
RunStats callers are unaffected.

Adds five containment tests: each seam failing in isolation, one bad row in a
batch of three leaving the other two linked, and errored not being conflated with
skipped. Verified all five fail if the containment is removed.

153 librarian tests pass; the hermetic harness run still exits 0.
@PRAteek-singHWY

Copy link
Copy Markdown
Contributor Author

Pushed. Both pipeline.py notes are addressed, and the branch now carries the #974 and #990 fixes so all three PRs show the same code.

Seams are typed. They were the only untyped parameters in the signature, next to an annotated threshold/pipeline_run_id and fully-typed decision_engine/emitter. Each is structurally one method, so they are now Protocols: KnowledgeSource, Retriever, Reranker, Scaler. Stubs stay as trivial as before while the call sites are checked. pipeline.py is clean under --strict mypy, with KnowledgeSource.items() typed to what section_from_queue_row actually accepts rather than a looser Mapping.

Per-row failures are contained. Only the C.0 boundary was guarded, so a failure from retrieve / rerank / confidence / decide / emit propagated and abandoned the whole batch along with every envelope already built. Those stages are stubs today but become live DB, embedding, and cross-encoder calls in W8, where one timeout must not cost the run. A failing row is now logged with its chunk and artifact id, counted in a new RunStats.errored, and the run continues. errored is separate from skipped on purpose: a boundary rejection is expected input hygiene, an error is a fault worth chasing. It defaults to 0, so existing RunStats callers are unaffected.

Five containment tests added: each seam failing in isolation, one bad row in a batch of three leaving the other two linked, and errored not being conflated with skipped. Verified all five fail if the containment is removed.

The two W8 notes on this PR stay open by design, as agreed in the threads above: the SafetyGuard flag wiring, and reconciling the queue row shape against Module B's live knowledge_queue (#989).

153 librarian tests pass; the hermetic harness run still exits 0.

@northdpole northdpole left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer review — Module C Week 6b (#991) — refresh

Verdict: Emitter + dry-run pipeline glue look good; same blockers as #990 (black + stack).

What looks solid

  • emit / build_link_proposal / build_review_item type-guard correctly; linked vs review envelopes are distinct
  • Auto-link uses Automatically linked to; review suggestions use Related — important so suggestions never claim auto-link
  • Injected at / pipeline_run_id keep runs reproducible
  • LibrarianPipeline per-row error containment (skipped vs errored) is the right shape for W8 live seams
  • Declared-degraded UpdateDetection until SafetyGuard — fine for dry-run W6b
  • Hermetic emitter + pipeline tests are in good shape

Blockers

  1. Lint / black — same failure class as #990 (evaluate_harness_test.py). Please black-format and push.
  2. Stack — sits on #990 / #974. Land those first (or rebase as they merge); do not merge W6b ahead of W6.

Non-blocking (before W8 persistence)

  • Protocol typing on constructor seams is nice (CodeRabbit); not required for dry-run merge
  • When persistence lands, keep auto-link vs suggested link types strictly separate in write paths

After black + stack resolve, approve-ready.

@northdpole

Copy link
Copy Markdown
Collaborator

#974 is merged. Please rebase onto main after #990 (or rebase the W6→W6b stack), run black, and push. Will re-review once CI is green.

@northdpole

Copy link
Copy Markdown
Collaborator

Rebase needed (blocking): CONFLICTING with main after #974 (~10 commits behind).

Please rebase onto main after #990 is rebased/green (or rebase the W6→W6b stack together), run black, and push. Will re-review once CI is green.

PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 8, 2026
…he live reports

report_decision_accuracy rebuilt the positive + hard_negative calibration set
from its own retrieve+rerank pass and fit its own temperature, duplicating what
report_calibration had already done a few lines earlier. On a live run that meant
a third pipeline pass over the calibration slices and two independent fits of the
same T, with nothing guaranteeing the two agreed.

Now there is one of each:

- calibration_set() extracts the (shortlist, label) derivation, so the C.3 gate
  and the C.4 report read the same pairs off the same shared audits.
- report_calibration returns (status, scaler); report_decision_accuracy takes the
  fitted scaler instead of fitting its own, so C.4 thresholds on exactly the T
  the ECE gate measured.
- A degenerate set yields (1, None) and C.4 is skipped with a message: no fitted
  T means no honest confidence to threshold, and the run has already failed.
- The live audit set now covers expected-decision rows too. C.4 grades those and
  they are not confined to the positive/hard_negative slices, so keying them off
  the calibration slices alone would have dropped them.

Adds evaluate_harness_test.py. The live reports only run under
--use_live_embeddings, so nothing exercised their wiring — which is exactly the
code that has to share one pipeline pass and one T across three reports. A
counting stub asserts the pipeline is called once per row and not once per
report, that only the two calibration slices enter the fit, and that a degenerate
set returns status 1 with no scaler rather than reporting success. Verified the
last one fails if the gate is flipped back to 0.

134 librarian tests pass; the hermetic harness run still exits 0.
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 8, 2026
…ions as auto-links

_proposed_links stamped link_type "Automatically linked to" on every ProposedLink,
including a ReviewItem's suggested_links — but those are candidates for a human to
consider, not auto-links. Parametrize the link type: LinkProposal.links use
AUTO_LINK_TYPE, ReviewItem.suggested_links use SUGGESTED_LINK_TYPE ("Related",
mirrors cre_defs.LinkTypes.Related). Test asserts a review suggestion is never
labelled as an auto-link.
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 8, 2026
…in per-row failures

Two nitpicks on pipeline.py, both aimed at the wiring rather than the dry-run
behaviour:

- The injected seams were the only untyped parameters in the signature, next to
  an annotated threshold/pipeline_run_id and fully-typed decision_engine/emitter.
  Each is structurally one method, so they are now Protocols — KnowledgeSource,
  Retriever, Reranker, Scaler — which keeps stubs as trivial as before while the
  call sites are checked. pipeline.py is clean under the --strict mypy the coding
  guidelines ask for; KnowledgeSource.items() is typed to what
  section_from_queue_row actually accepts, not a looser Mapping.

- Only the C.0 boundary was guarded, so a failure from retrieve / rerank /
  confidence / decide / emit propagated and abandoned the whole batch along with
  every envelope already built. Those stages are stubs today but become live DB,
  embedding, and cross-encoder calls in W8, where one timeout must not cost the
  run. Failures are now contained per row: the row is logged with its chunk and
  artifact id, counted in a new RunStats.errored, and the run continues.

errored is separate from skipped on purpose — a boundary rejection is expected
input hygiene, an error is a fault worth chasing. It defaults to 0, so existing
RunStats callers are unaffected.

Adds five containment tests: each seam failing in isolation, one bad row in a
batch of three leaving the other two linked, and errored not being conflated with
skipped. Verified all five fail if the containment is removed.

153 librarian tests pass; the hermetic harness run still exits 0.

@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

🤖 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/__init__.py`:
- Around line 21-24: Update the C.4 scope statement in the module documentation
to remove the claim that the envelope emitter and pipeline glue are unbuilt,
since emitter.py and pipeline.py now provide them. Keep the statement that only
the W8 queue and graph writers remain unbuilt.
🪄 Autofix

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: 7adc8589-344b-416e-bcef-3bfb9538345b

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9fda5 and 332db47.

📒 Files selected for processing (5)
  • application/tests/librarian/evaluate_harness_test.py
  • application/tests/librarian/pipeline_test.py
  • application/utils/librarian/__init__.py
  • application/utils/librarian/pipeline.py
  • scripts/evaluate_librarian.py

Comment thread application/utils/librarian/__init__.py Outdated
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 8, 2026
…ions as auto-links

_proposed_links stamped link_type "Automatically linked to" on every ProposedLink,
including a ReviewItem's suggested_links — but those are candidates for a human to
consider, not auto-links. Parametrize the link type: LinkProposal.links use
AUTO_LINK_TYPE, ReviewItem.suggested_links use SUGGESTED_LINK_TYPE ("Related",
mirrors cre_defs.LinkTypes.Related). Test asserts a review suggestion is never
labelled as an auto-link.
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 8, 2026
…in per-row failures

Two nitpicks on pipeline.py, both aimed at the wiring rather than the dry-run
behaviour:

- The injected seams were the only untyped parameters in the signature, next to
  an annotated threshold/pipeline_run_id and fully-typed decision_engine/emitter.
  Each is structurally one method, so they are now Protocols — KnowledgeSource,
  Retriever, Reranker, Scaler — which keeps stubs as trivial as before while the
  call sites are checked. pipeline.py is clean under the --strict mypy the coding
  guidelines ask for; KnowledgeSource.items() is typed to what
  section_from_queue_row actually accepts, not a looser Mapping.

- Only the C.0 boundary was guarded, so a failure from retrieve / rerank /
  confidence / decide / emit propagated and abandoned the whole batch along with
  every envelope already built. Those stages are stubs today but become live DB,
  embedding, and cross-encoder calls in W8, where one timeout must not cost the
  run. Failures are now contained per row: the row is logged with its chunk and
  artifact id, counted in a new RunStats.errored, and the run continues.

errored is separate from skipped on purpose — a boundary rejection is expected
input hygiene, an error is a fault worth chasing. It defaults to 0, so existing
RunStats callers are unaffected.

Adds five containment tests: each seam failing in isolation, one bad row in a
batch of three leaving the other two linked, and errored not being conflated with
skipped. Verified all five fail if the containment is removed.

153 librarian tests pass; the hermetic harness run still exits 0.
PRAteek-singHWY added a commit to PRAteek-singHWY/OpenCRE that referenced this pull request Aug 8, 2026
…ement

The package docstring still listed the envelope emitter and the C.0->C.4
pipeline glue as not built, but this is the branch that builds them — the same
staleness the W6 scope line was corrected for on OWASP#990.

Adds the W6b line for the emitter and the glue (noting the pipeline is dry-run),
and narrows the not-yet marker to what is genuinely still missing: the live
queue drain and the graph / review-queue writers in W8.

@northdpole northdpole left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review (stacked on #990)

Prior rebase/black blocker cleared. Confirmed this tip is ahead of #990 (proper stack, no duplicate W5 copies) and CI is green.

Verified

  • Emitter builds RFC LinkProposal / ReviewItem with injected timestamps; review suggestions use Related (not auto-link labelling) — good.
  • Pipeline wires C.0→C.4 dry-run with typed Protocol seams; per-row error containment + skipped-vs-errored stats are tested.
  • Package docstring now lists W6b emitter/pipeline; “not built” marker correctly moved to W8 writers.
  • Prior notes on SafetyGuard flags / B↔C queue shape remain pre-W8 — fine to leave for later.

Approve. Merge with rebase after #990 lands (stack order #990#991).

northdpole pushed a commit that referenced this pull request Aug 9, 2026
…ve reports

report_decision_accuracy rebuilt the positive + hard_negative calibration set
from its own retrieve+rerank pass and fit its own temperature, duplicating what
report_calibration had already done a few lines earlier. On a live run that meant
a third pipeline pass over the calibration slices and two independent fits of the
same T, with nothing guaranteeing the two agreed.

Now there is one of each:

- calibration_set() extracts the (shortlist, label) derivation, so the C.3 gate
  and the C.4 report read the same pairs off the same shared audits.
- report_calibration returns (status, scaler); report_decision_accuracy takes the
  fitted scaler instead of fitting its own, so C.4 thresholds on exactly the T
  the ECE gate measured.
- A degenerate set yields (1, None) and C.4 is skipped with a message: no fitted
  T means no honest confidence to threshold, and the run has already failed.
- The live audit set now covers expected-decision rows too. C.4 grades those and
  they are not confined to the positive/hard_negative slices, so keying them off
  the calibration slices alone would have dropped them.

Adds evaluate_harness_test.py. The live reports only run under
--use_live_embeddings, so nothing exercised their wiring — which is exactly the
code that has to share one pipeline pass and one T across three reports. A
counting stub asserts the pipeline is called once per row and not once per
report, that only the two calibration slices enter the fit, and that a degenerate
set returns status 1 with no scaler rather than reporting success. Verified the
last one fails if the gate is flipped back to 0.

134 librarian tests pass; the hermetic harness run still exits 0.
…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.
…ions as auto-links

_proposed_links stamped link_type "Automatically linked to" on every ProposedLink,
including a ReviewItem's suggested_links — but those are candidates for a human to
consider, not auto-links. Parametrize the link type: LinkProposal.links use
AUTO_LINK_TYPE, ReviewItem.suggested_links use SUGGESTED_LINK_TYPE ("Related",
mirrors cre_defs.LinkTypes.Related). Test asserts a review suggestion is never
labelled as an auto-link.
…in per-row failures

Two nitpicks on pipeline.py, both aimed at the wiring rather than the dry-run
behaviour:

- The injected seams were the only untyped parameters in the signature, next to
  an annotated threshold/pipeline_run_id and fully-typed decision_engine/emitter.
  Each is structurally one method, so they are now Protocols — KnowledgeSource,
  Retriever, Reranker, Scaler — which keeps stubs as trivial as before while the
  call sites are checked. pipeline.py is clean under the --strict mypy the coding
  guidelines ask for; KnowledgeSource.items() is typed to what
  section_from_queue_row actually accepts, not a looser Mapping.

- Only the C.0 boundary was guarded, so a failure from retrieve / rerank /
  confidence / decide / emit propagated and abandoned the whole batch along with
  every envelope already built. Those stages are stubs today but become live DB,
  embedding, and cross-encoder calls in W8, where one timeout must not cost the
  run. Failures are now contained per row: the row is logged with its chunk and
  artifact id, counted in a new RunStats.errored, and the run continues.

errored is separate from skipped on purpose — a boundary rejection is expected
input hygiene, an error is a fault worth chasing. It defaults to 0, so existing
RunStats callers are unaffected.

Adds five containment tests: each seam failing in isolation, one bad row in a
batch of three leaving the other two linked, and errored not being conflated with
skipped. Verified all five fail if the containment is removed.

153 librarian tests pass; the hermetic harness run still exits 0.
…ement

The package docstring still listed the envelope emitter and the C.0->C.4
pipeline glue as not built, but this is the branch that builds them — the same
staleness the W6 scope line was corrected for on OWASP#990.

Adds the W6b line for the emitter and the glue (noting the pipeline is dry-run),
and narrows the not-yet marker to what is genuinely still missing: the live
queue drain and the graph / review-queue writers in W8.
@northdpole
northdpole force-pushed the gsocmodule_C_week_6b branch from 5f2d75b to 9baa415 Compare August 9, 2026 20:22
@northdpole

Copy link
Copy Markdown
Collaborator

Maintainer note: #990 is merged. I rebased this branch onto latest main (dropped the already-landed W6 commits; 5 W6b commits remain) and force-pushed to gsocmodule_C_week_6b so we can land the stack. Waiting on CI, then rebase-merge.

@northdpole
northdpole merged commit 7ae8671 into OWASP:main Aug 9, 2026
6 checks passed
northdpole pushed a commit that referenced this pull request Aug 9, 2026
…as auto-links

_proposed_links stamped link_type "Automatically linked to" on every ProposedLink,
including a ReviewItem's suggested_links — but those are candidates for a human to
consider, not auto-links. Parametrize the link type: LinkProposal.links use
AUTO_LINK_TYPE, ReviewItem.suggested_links use SUGGESTED_LINK_TYPE ("Related",
mirrors cre_defs.LinkTypes.Related). Test asserts a review suggestion is never
labelled as an auto-link.
northdpole pushed a commit that referenced this pull request Aug 9, 2026
…r-row failures

Two nitpicks on pipeline.py, both aimed at the wiring rather than the dry-run
behaviour:

- The injected seams were the only untyped parameters in the signature, next to
  an annotated threshold/pipeline_run_id and fully-typed decision_engine/emitter.
  Each is structurally one method, so they are now Protocols — KnowledgeSource,
  Retriever, Reranker, Scaler — which keeps stubs as trivial as before while the
  call sites are checked. pipeline.py is clean under the --strict mypy the coding
  guidelines ask for; KnowledgeSource.items() is typed to what
  section_from_queue_row actually accepts, not a looser Mapping.

- Only the C.0 boundary was guarded, so a failure from retrieve / rerank /
  confidence / decide / emit propagated and abandoned the whole batch along with
  every envelope already built. Those stages are stubs today but become live DB,
  embedding, and cross-encoder calls in W8, where one timeout must not cost the
  run. Failures are now contained per row: the row is logged with its chunk and
  artifact id, counted in a new RunStats.errored, and the run continues.

errored is separate from skipped on purpose — a boundary rejection is expected
input hygiene, an error is a fault worth chasing. It defaults to 0, so existing
RunStats callers are unaffected.

Adds five containment tests: each seam failing in isolation, one bad row in a
batch of three leaving the other two linked, and errored not being conflated with
skipped. Verified all five fail if the containment is removed.

153 librarian tests pass; the hermetic harness run still exits 0.
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.

2 participants