week_6: Module C (The Librarian) — C.4 decision engine + golden-set decision gate - #990
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Summary by CodeRabbit
WalkthroughAdds temperature-scaling calibration, confidence-based link/review decisions, hermetic tests, and live evaluation reporting with shared retrieval and reranking audits. ChangesLibrarian calibration and routing
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
application/utils/librarian/calibration/temperature.py (1)
63-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor duplication between
_softmax_topandprobabilities.Both implement the same "asarray + empty check + softmax" logic independently.
confidence()could derive fromprobabilities()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 liftLive retrieve+rerank (and the temperature fit) is redundantly recomputed 2-3x per row.
report_decision_accuracyrebuilds the exact samecal_rowsset and rerunsretriever.retrieve/reranker.rerankper row to refit a secondTemperatureScaler, duplicating workreport_calibration(called right before it inmain, L458-459) already did. Then its owngradedloop rerunsretrieve/rerankagain for rows that overlap withcal_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_calibrationreturn the fittedTemperatureScaler(and/or the per-row audits) soreport_decision_accuracyreuses them instead of recomputing, and caching each row'sretrieve+rerankresult by row id so thecal_rows/gradedloops 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
📒 Files selected for processing (7)
application/tests/librarian/decision_engine_test.pyapplication/tests/librarian/temperature_test.pyapplication/utils/librarian/__init__.pyapplication/utils/librarian/calibration/__init__.pyapplication/utils/librarian/calibration/temperature.pyapplication/utils/librarian/decision_engine.pyscripts/evaluate_librarian.py
…string The calibration/__init__.py docstring still described the rejected single-logit `p = sigmoid(z/T)` approach. temperature.py actually calibrates the softmax over the whole shortlist (`p = softmax(logits / T)`, confidence = top-1 mass) — the sigmoid-on-one-logit approach cannot be calibrated by temperature alone. Docstring now matches the implementation.
northdpole
left a comment
There was a problem hiding this comment.
Maintainer review — Module C Week 6 (#990)
Decision engine looks solid: precedence, inclusive τ, guards, and table tests are clean. No blocking logic bugs in the unique Week-6 surface.
Depends on #974 for the stack base (please rebase after that gate fix lands). One docstring nit inline.
Non-blocking note: report_decision_accuracy intentionally always returns 0 (informational until SafetyGuard + τ tuning) — fine; just don't confuse that with the ECE gate in #974.
| Calibration + decision routing (C.3-C.4, W5-W6) onward is not built yet. | ||
| W5 (C.3): confidence calibration — temperature scaling maps a rerank logit to | ||
| an honest probability (fit by NLL on the golden set, gated ECE < 0.10). | ||
| Decision routing (C.4, W6) onward is not built yet. |
There was a problem hiding this comment.
Nit — package docstring is stale
This still says "Decision routing (C.4, W6) onward is not built yet." Week 6 adds decide() here. Please update the scope blurb to mention C.4 / W6 (and leave W6b emitter/pipeline / W8 writers as not-yet if you prefer).
There was a problem hiding this comment.
Fixed in 86d3d5b5. Added the W6 (C.4) scope line for decide() and moved the not-yet marker down to the W6b emitter/pipeline glue and the W8 queue/graph writers. Will rebase onto gsocmodule_C_week_5 once #974 lands.
…tring scope The application/utils/librarian package docstring still said "Decision routing (C.4, W6) onward is not built yet", but W6 adds the C.4 decision engine (decide()) in this package. Add the W6 (C.4) scope line and move the not-yet marker to the W6b emitter/pipeline glue and the W8 queue/graph writers.
…hortlists, dedupe softmax Three nitpicks from the bot review, none of which changed a metric: - evaluate_librarian: the live retrieve+rerank was recomputed per report. The models were already built once in main, but report_retrieval_recall and report_calibration each re-ran the pipeline over the positive slice, so every positive row paid for two cross-encoder passes. live_audits() now retrieves and reranks each row once, keyed by golden row id, and both reports read the same shortlists. Rows without an audit no longer count toward a report's denominator, so the printed fractions cannot divide by unscored rows. - temperature: _softmax_top duplicated the empty-shortlist guard and the softmax already in TemperatureScaler.probabilities. Both now route through one _softmax_at helper, and confidence() derives from probabilities() so the two can never disagree. - temperature is now clean under the --strict mypy the coding guidelines ask for: annotated _paired's return and the bounds tuple, re-asserted the array type over untyped scipy, and hoisted the label conversion out of the fit objective (it was re-validated on every optimiser iteration anyway). Behaviour is unchanged: 113 librarian tests pass and the hermetic harness run still exits 0.
…hortlists, dedupe softmax Three nitpicks from the bot review, none of which changed a metric: - evaluate_librarian: the live retrieve+rerank was recomputed per report. The models were already built once in main, but report_retrieval_recall and report_calibration each re-ran the pipeline over the positive slice, so every positive row paid for two cross-encoder passes. live_audits() now retrieves and reranks each row once, keyed by golden row id, and both reports read the same shortlists. Rows without an audit no longer count toward a report's denominator, so the printed fractions cannot divide by unscored rows. - temperature: _softmax_top duplicated the empty-shortlist guard and the softmax already in TemperatureScaler.probabilities. Both now route through one _softmax_at helper, and confidence() derives from probabilities() so the two can never disagree. - temperature is now clean under the --strict mypy the coding guidelines ask for: annotated _paired's return and the bounds tuple, re-asserted the array type over untyped scipy, and hoisted the label conversion out of the fit objective (it was re-validated on every optimiser iteration anyway). Behaviour is unchanged: 113 librarian tests pass and the hermetic harness run still exits 0. (cherry picked from commit 2bbc76e)
…hortlists, dedupe softmax Three nitpicks from the bot review, none of which changed a metric: - evaluate_librarian: the live retrieve+rerank was recomputed per report. The models were already built once in main, but report_retrieval_recall and report_calibration each re-ran the pipeline over the positive slice, so every positive row paid for two cross-encoder passes. live_audits() now retrieves and reranks each row once, keyed by golden row id, and both reports read the same shortlists. Rows without an audit no longer count toward a report's denominator, so the printed fractions cannot divide by unscored rows. - temperature: _softmax_top duplicated the empty-shortlist guard and the softmax already in TemperatureScaler.probabilities. Both now route through one _softmax_at helper, and confidence() derives from probabilities() so the two can never disagree. - temperature is now clean under the --strict mypy the coding guidelines ask for: annotated _paired's return and the bounds tuple, re-asserted the array type over untyped scipy, and hoisted the label conversion out of the fit objective (it was re-validated on every optimiser iteration anyway). Behaviour is unchanged: 113 librarian tests pass and the hermetic harness run still exits 0. (cherry picked from commit 2bbc76e) (cherry picked from commit ae6bb69)
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
application/tests/librarian/evaluate_harness_test.py (1)
211-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExercise the decision-report behavior that these tests name.
test_grades_expected_decision_rows_off_shared_auditspasses ifreport_decision_accuracy()returns0without reporting any metrics. Assert the reported counts or return structured metrics.
test_no_graded_rows_is_not_an_errorcreates an audit for a row that_golden_row()marks aslinked. Thegradedlist is therefore nonempty. Pass an empty audit map to execute the no-graded-rows branch.As per coding guidelines, “New behavior and importers should follow a test-first workflow.”
🤖 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/tests/librarian/evaluate_harness_test.py` around lines 211 - 247, Strengthen the tests around harness.report_decision_accuracy by asserting its reported decision metrics or structured result, rather than only its status and pipeline-call counts. In test_no_graded_rows_is_not_an_error, pass an empty audit map so the no-graded-rows branch is exercised instead of creating a linked audit from _golden_row; preserve the expected zero status.Source: Coding guidelines
🤖 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/tests/librarian/evaluate_harness_test.py`:
- Line 123: Update the assignment from harness.report_calibration in the test to
bind the unused status result to _, while preserving the scaler binding and
existing test behavior.
In `@scripts/evaluate_librarian.py`:
- Around line 110-113: Update the live-evaluation descriptions in
_build_live_pipeline(), the --use_live_embeddings help text, and the offline
message to state that the live pipeline produces the C.4 decision report and may
return a nonzero calibration status.
---
Nitpick comments:
In `@application/tests/librarian/evaluate_harness_test.py`:
- Around line 211-247: Strengthen the tests around
harness.report_decision_accuracy by asserting its reported decision metrics or
structured result, rather than only its status and pipeline-call counts. In
test_no_graded_rows_is_not_an_error, pass an empty audit map so the
no-graded-rows branch is exercised instead of creating a linked audit from
_golden_row; preserve the expected zero status.
🪄 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: e0d5434e-4076-49a9-b00c-d711340a56dc
📒 Files selected for processing (3)
application/tests/librarian/evaluate_harness_test.pyapplication/utils/librarian/calibration/temperature.pyscripts/evaluate_librarian.py
🚧 Files skipped from review as they are similar to previous changes (1)
- application/utils/librarian/calibration/temperature.py
|
Pushed on top of the docstring fix. Carried the #974 fixes onto this branch. This branch is cumulative off
Adds 134 librarian tests pass; the hermetic harness run still exits 0. |
There was a problem hiding this comment.
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/tests/librarian/dataset_test.py`:
- Around line 113-120: Update _load_harness to validate that spec from
spec_from_file_location is not None before passing it to module_from_spec or
accessing its loader. If the specification cannot be created, raise a clear
error indicating that scripts/evaluate_librarian.py is missing.
🪄 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: e37bcc6d-95de-4455-a3ae-cf975c7b6f14
📒 Files selected for processing (3)
application/tests/librarian/dataset_test.pyapplication/tests/librarian/evaluate_harness_test.pyscripts/evaluate_librarian.py
🚧 Files skipped from review as they are similar to previous changes (1)
- application/tests/librarian/evaluate_harness_test.py
northdpole
left a comment
There was a problem hiding this comment.
Maintainer review — Module C Week 6 (#990) — refresh
Verdict: Logic/tests look good; not mergeable yet until black is green and the Week-5 base is settled.
What looks solid
decide()is the right size: pure, model-free, inclusive>= τ, top-1 only, frozenDecisionResult- Reason-code precedence matches the RFC story:
NO_CANDIDATES > ADVERSARIAL_FLAG > UPDATE_AMBIGUOUS > BELOW_THRESHOLD - Hermetic table tests cover boundary, flags, guards, and precedence
- Declared-degraded SafetyGuard flags (accepted, not produced) is clearly called out — fine for W6
- Decision-accuracy report staying informational (always status 0) is the right call until W7 τ sweep / SafetyGuard
Blockers
- Lint / black — CI fails on
application/tests/librarian/evaluate_harness_test.py(would reformat). Please run black and push. - Stack — still depends on #974 (CHANGES_REQUESTED). Rebase onto
mainafter that lands so the Week-6-only surface is what we merge.
Non-blocking
- Confirm harness tests assert the graded decision outcomes they name (CodeRabbit called this out earlier) — nice-to-have, not a gate for W6.
Once black is green and #974 is in (or this is rebased cleanly), this is approve-ready.
… dedupe softmax Three nitpicks from the bot review, none of which changed a metric: - evaluate_librarian: the live retrieve+rerank was recomputed per report. The models were already built once in main, but report_retrieval_recall and report_calibration each re-ran the pipeline over the positive slice, so every positive row paid for two cross-encoder passes. live_audits() now retrieves and reranks each row once, keyed by golden row id, and both reports read the same shortlists. Rows without an audit no longer count toward a report's denominator, so the printed fractions cannot divide by unscored rows. - temperature: _softmax_top duplicated the empty-shortlist guard and the softmax already in TemperatureScaler.probabilities. Both now route through one _softmax_at helper, and confidence() derives from probabilities() so the two can never disagree. - temperature is now clean under the --strict mypy the coding guidelines ask for: annotated _paired's return and the bounds tuple, re-asserted the array type over untyped scipy, and hoisted the label conversion out of the fit objective (it was re-validated on every optimiser iteration anyway). Behaviour is unchanged: 113 librarian tests pass and the hermetic harness run still exits 0.
|
#974 is merged. Please rebase this PR onto |
…hortlists, dedupe softmax Three nitpicks from the bot review, none of which changed a metric: - evaluate_librarian: the live retrieve+rerank was recomputed per report. The models were already built once in main, but report_retrieval_recall and report_calibration each re-ran the pipeline over the positive slice, so every positive row paid for two cross-encoder passes. live_audits() now retrieves and reranks each row once, keyed by golden row id, and both reports read the same shortlists. Rows without an audit no longer count toward a report's denominator, so the printed fractions cannot divide by unscored rows. - temperature: _softmax_top duplicated the empty-shortlist guard and the softmax already in TemperatureScaler.probabilities. Both now route through one _softmax_at helper, and confidence() derives from probabilities() so the two can never disagree. - temperature is now clean under the --strict mypy the coding guidelines ask for: annotated _paired's return and the bounds tuple, re-asserted the array type over untyped scipy, and hoisted the label conversion out of the fit objective (it was re-validated on every optimiser iteration anyway). Behaviour is unchanged: 113 librarian tests pass and the hermetic harness run still exits 0.
…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.
…string The calibration/__init__.py docstring still described the rejected single-logit `p = sigmoid(z/T)` approach. temperature.py actually calibrates the softmax over the whole shortlist (`p = softmax(logits / T)`, confidence = top-1 mass) — the sigmoid-on-one-logit approach cannot be calibrated by temperature alone. Docstring now matches the implementation.
…tring scope The application/utils/librarian package docstring still said "Decision routing (C.4, W6) onward is not built yet", but W6 adds the C.4 decision engine (decide()) in this package. Add the W6 (C.4) scope line and move the not-yet marker to the W6b emitter/pipeline glue and the W8 queue/graph writers.
…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.
Two follow-ups from the bot review of the last push, neither behavioural: - The live-path descriptions still predated C.4. The module docstring claimed the semantic path was stubbed, _build_live_pipeline named only recall and calibration as its consumers, --use_live_embeddings help listed only recall and top-1, and the offline message omitted the decision report. All four now say what the run actually does, including that C.3 is the one live report that sets the exit status (a failed or skipped gate returns nonzero) while C.4 is informational until SafetyGuard and tau tuning land. - evaluate_harness_test bound the calibration status it never asserted (Ruff RUF059). Bound to _status: the test is about the pipeline not being re-run and the scaler coming back, and the gate outcome on stub logits is not a meaningful assertion. ruff check is clean on both files. 136 librarian tests pass; the hermetic harness run still exits 0.
0968e7e to
6b0da0f
Compare
`_load_harness` fed `spec_from_file_location` straight into `module_from_spec` and `spec.loader.exec_module`. Both can be None — a missing path yields no spec, and a spec can carry no loader — so a harness that failed to load surfaced as an AttributeError on None, naming neither the file nor the cause. Raises ImportError with the path instead. Deliberately not a skip: the harness is committed, so a load failure is a real breakage, and skipping would report success for tests that never ran — the same greenwashing the W5 calibration gate was fixed for.
…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
left a comment
There was a problem hiding this comment.
Re-review (post-rebase)
Prior blocker cleared: MERGEABLE on main, black/lint clean, all 6 checks green. Thanks for the Slack ping and the stack cleanup.
Verified
decide()is a small total function with clear reason-code precedence; hermetic table tests look solid.- Harness shares one retrieve+rerank pass and one fitted
Tacross C.3/C.4 reports; C.4 stays informational; degenerate calibration still fails the live run (no greenwash). - CodeRabbit harness-load note addressed in
dataset_test._load_harness:ImportErrorwith path, no skip — correct call. - Package / calibration docstrings refreshed for W6 scope.
Optional follow-up (non-blocking)
evaluate_harness_test.py still uses a bare assert _spec and _spec.loader at import time; aligning it with the same ImportError(f"... {path}") pattern as dataset_test would be nice consistency, not a merge blocker.
Approve. Merge with rebase after CI; then land #991 (stacked on this).
The calibration/__init__.py docstring still described the rejected single-logit `p = sigmoid(z/T)` approach. temperature.py actually calibrates the softmax over the whole shortlist (`p = softmax(logits / T)`, confidence = top-1 mass) — the sigmoid-on-one-logit approach cannot be calibrated by temperature alone. Docstring now matches the implementation.
… scope The application/utils/librarian package docstring still said "Decision routing (C.4, W6) onward is not built yet", but W6 adds the C.4 decision engine (decide()) in this package. Add the W6 (C.4) scope line and move the not-yet marker to the W6b emitter/pipeline glue and the W8 queue/graph writers.
…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.
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 #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.
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.
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
Tthat maps the reranked shortlist to a trustworthyconfidence = 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 iffconfidence >= thresholdand there is a candidate and no blocking safety flag; otherwise it routes to review with areason_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 bydecide()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
decision_engine.py(new)decide(confidence, candidate_cre_ids, *, threshold, adversarial, update_ambiguous) -> DecisionResult. Links the top-1 iffconfidence >= thresholdAND candidates exist AND no blocking flag; else reviews with areason_code. FrozenDecisionResult, versionedENGINE_NAME, customDecisionError, input guards on confidence/threshold. Model-free and confidence-agnostic so it is hermetically testable - mirrors the C.1embed_fn/ C.2score_fn/ C.3 scaler seams.evaluate_librarian.pyreport_decision_accuracy: fitsTon 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 attau=0.80the 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: tuningtauis the Week-7 experiment, and flag-based reason codes lag until the SafetyGuard lands.report_calibration(W5) untouched.decision_engine_test.py(new)>=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"]Results
Reading the C.4 line by direction, because a single accuracy hides the story:
tau=0.80many 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.What is intentionally not here
DecisionResultinto the RFCLinkProposal/ReviewItemand wiring C.0→C.4. Ships stacked asweek_6b.ood/conformal/ update-detection) that would populateadversarial/update_ambiguous.Tfor the live decision path, and live B→C integration + graph writes (W8) - the pipeline stays dry-run.How to verify locally