Skip to content

week_8b: Module C (The Librarian) — docs, final metrics, and the regression gate - #1012

Closed
PRAteek-singHWY wants to merge 11 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_8b
Closed

week_8b: Module C (The Librarian) — docs, final metrics, and the regression gate#1012
PRAteek-singHWY wants to merge 11 commits into
OWASP:mainfrom
PRAteek-singHWY:gsocmodule_C_week_8b

Conversation

@PRAteek-singHWY

Copy link
Copy Markdown
Contributor

Hi @northdpole — Week 8b of Module C, and the last piece of the plan that is not code: package docs, the runbook, measured final metrics, the GSoC final report, and the CI gate.

Stacked on #1011 (week_8). Until that merges the diff shows its commits too; I'll rebase onto main as it lands, shrinking this to the docs-only surface. Stack order: #1011 → this.

No application code changes. One .gitignore edit, one workflow, four markdown files.

Overview

Module C is complete through C.4 and now drains B's live queue (#1011). What was missing was everything a maintainer needs to operate and trust it: how to run it, what each knob means, what the numbers actually are, and a gate that stops those numbers rotting.

What changed

Area Files Description
Package README application/utils/librarian/README.md The C.-1 → C.4 stages, the supporting live-path modules, the two invariants (consumption gated on persistence; errors ≠ refusals), and the design constraints — seams not implementations, no DB import at module scope, declared-degraded over silently-degraded.
Runbook docs/gsoc_2026_module_c/runbook.md Three ways to run C, every CLI flag, all 8 CRE_LIBRARIAN_* variables, the pgvector cache migration, and a troubleshooting section keyed to the actual error strings.
Final metrics docs/gsoc_2026_module_c/final_metrics.md Every number reproducible from one command, with the environment stated.
Final report docs/gsoc_2026_module_c/final_report.md The GSoC write-up: what was built week by week, design decisions worth defending, results, integration status, what is not built, and recommendations.
Regression gate .github/workflows/librarian_regression.yml Hermetic CI: the librarian unit suite plus the golden-set decision gate, path-filtered to Module C.
gitignore .gitignore *.md is blanket-ignored, so the new docs need explicit negations — the same pattern docs/Mid_eval_blog_gsoc2026/module_B_mideval_blog.md already uses. Nothing else is un-ignored.

The measured numbers

Stage Result Target
C.0 boundary validation 319/319 (100%)
C.0.5 explicit resolver 5/5 (100%) 100%
C.1 retrieval recall@20 285/292 (98%) any-hit
C.2 rerank top-1 220/292 (75%) ≥ 90%
C.3 calibration ECE 0.046 (fitted T=1.105) < 0.10
C.4 review recall 5/5 (100%)
C.4 auto-link recall 176/314 (56%)

Read C.4 by direction. Review recall is 5/5 — every chunk that should reach a human does, and the engine never wrongly auto-links something that needed review. For a gate whose job is protecting a shared graph, that is the number that matters. Auto-link recall of 56% is the cost: at τ=0.80 many correct-but-close positives fall under the bar and route to review, which is the safe direction.

On the missed top-1 target, stated plainly rather than rounded away. Week 8's investigation tried thirteen reranker levers and all thirteen regressed — C.2 as shipped scores net −7 against plain cosine on the same shortlist. The cause is not the model: 427 of 428 CREs have empty description fields, and a cross-encoder cannot cross-attend to a bare title. C.1 still reaches 98% recall over that same corpus, which localises the problem precisely — the information needed to rank within a shortlist largely is not present. The path to 90% runs through populating CRE descriptions, and that is a corpus problem upstream of Module C.

W9's selective reranking (250/319 top-1 vs the shipped 238, precision held at τ=0.80) is documented as measured but deliberately unshipped: it needs two calibrators and held-out validation. Shipping it on in-sample numbers would be the same greenwashing this module spent four separate fixes removing.

Why the CI gate is hermetic

No DB, key, or model download. The semantic reports need live CRE vectors, and seeding a candidate pool from golden text offline is exactly the leakage the hub firewall exists to strip — a CI job that "measured" C.2 offline would be measuring nothing.

What it does catch is the failure that actually bit #1011: a Module B schema change silently rejecting every row at the C.0 boundary, leaving the explicit gate with nothing to count while the run still exited 0.

What the report records as not built

  • Graph / review-queue writers (W8b) — C emits envelopes; no link is committed.
  • The SafetyGuard detector — the seam is wired, the detector is future work.
  • Selective reranking — measured, promising, unvalidated.

It also records that A → B is not yet connected (Module A does not write harvest_input), so the end-to-end chain is not demonstrable outside a seeded record. That belongs in the report rather than in a claim that the pipeline is whole.

How to verify locally

python -m unittest discover -s application/tests/librarian -p '*_test.py' -t .
python scripts/evaluate_librarian.py \
    --dataset application/tests/librarian/fixtures/golden_dataset.json

# the live numbers in final_metrics.md (needs a populated cache + embedding LLM)
python scripts/rewrite_sqlite_embeddings_to_vec.py --db standards_cache.sqlite
python scripts/evaluate_librarian.py \
    --dataset application/tests/librarian/fixtures/golden_dataset.json \
    --use_live_embeddings --cache_file standards_cache.sqlite

…dule B's merged table

Module B's `knowledge_queue` (OWASP#989) is substantially richer than the flat mirror
C was built against. C modelled `source_repo` / `source_path` /
`source_commit_sha` and nothing else, so against the merged table it failed
validation on 100% of real rows, had no `chunk_id` or `artifact_id` to carry, and
could not represent an rss row at all.

`schemas.KnowledgeQueueItem` now mirrors B's table column for column — all 23,
nothing dropped and nothing invented — and `section_from_queue_row` maps both
provenance branches:

  github : source_repo / source_commit_sha / source_committed_at, locator repo_path
  rss    : feed_url / post_guid,                                 locator feed_item

The identity columns are the point. `chunk_id` and `artifact_id` originate in
Module A and travel through B untouched; C now uses them verbatim instead of
synthesising its own, so an envelope refers to the same chunk every other module
means.

`DbKnowledgeSource` reads B's own SQLAlchemy model rather than redeclaring the
table — C owns no schema and ships no migration. A row B wrote that C cannot
model is logged with its ids and skipped rather than aborting the batch: a
contract breach is worth seeing, but it must not cost the run.
…ates consumption

Retiring a queue row tells Module B never to offer that chunk again. Doing that
while the envelope built from it goes nowhere destroys the chunk outright: B will
not re-offer the row and nothing downstream ever saw the decision. So consumption
is defined against a sink, not against the pipeline having finished.

  JsonlEnvelopeSink — appends one RFC envelope per line; fsynced, append-only so
                      several runs can share a file. `persists=True`.
  NullEnvelopeSink  — counts and discards, for dry runs. `persists=False`, and
                      the runner refuses to consume behind it.

The graph / review-queue writers (W8b) become further implementations of the same
protocol, so the consumption rule does not have to change when they land.

Envelopes are serialised with `exclude_none=True`, which is not cosmetic. The
vendored RFC schemas type their optional fields as plain "string" and leave them
out of `required`, so an absent value must be an absent *key*. Emitting
`"repo": null` fails validation against the very schema Module D reads — every
rss envelope trips it on repo/commit_sha, every github one on feed_url/post_guid.
No data is lost by omitting them: those are exactly the columns B itself stores
NULL per row type, which its own contract documents as `github only; NULL for
rss`.

Two tests pin this, because the old tests could not have caught it — asserting
"one JSON object per line" passes happily on a file whose every record is
invalid. One validates a written envelope against the vendored schema, resolved
in `$id` space so link-proposal's local `#/$defs` pointers resolve; the other
asserts no null survives anywhere in the tree. Both fail if `exclude_none` is
removed.
The mirror image of Module B's queue_writer. B inserts rows and C retires them;
`db.KnowledgeQueueItem`'s own docstring assigns this side to C.

This is the only place Module C writes to Module B's table, and it writes exactly
one column. It never deletes a row: the queue doubles as the audit trail of what B
handed over and what C did with it.

Idempotent by construction — the update is filtered on `consumed_at IS NULL`, so
replaying a run cannot move a timestamp that is already set. The returned count is
rows actually stamped, which is why it can be lower than the number of ids passed
in: a concurrent run got there first, or the id no longer exists. That is reported
rather than raised.

Ids are chunked before the `IN (...)`, because Postgres has a bind-parameter
ceiling and a large IN plans badly.

The caller owns the transaction, matching `run_noise_filter` on B's side.
…de()

`decide()` has accepted `adversarial` / `update_ambiguous` since W6, but no caller
ever passed them, so ADVERSARIAL_FLAG and UPDATE_AMBIGUOUS could not fire from the
pipeline. That was flagged on OWASP#991 as something that must be wired before any
write-back, since an auto-link that skips the safety path is exactly the failure
the flags exist to prevent. This is that wiring.

What is deliberately *not* here is a detector. The real guard — out-of-distribution
scoring, conformal prediction, update detection — is later work. So the seam ships
with NullSafetyGuard, which evaluates nothing and says so: its verdict carries
`evaluated=False`, the pipeline counts those rows, and the runner reports the
count.

That distinction is the whole point. W5's review turned on a gate that skipped and
still reported success; an unevaluated safety path that looks identical to a clean
one is the same failure wearing a different hat. A clean verdict from a guard that
never ran is a default, not a finding, and the run log now says which one it is.

The rule this establishes for W8b: a writer that commits links into the graph must
refuse to run behind a guard reporting `evaluated=False`. Retiring a queue row
without the safety path is recoverable — the envelope is still on disk. Committing
a link into a graph other tools read as truth is not.
…ator

Until now the only place that knew how to construct a real retriever, reranker and
scaler was `cre_main.run_librarian`, inline. That is why the OIE orchestrator (OWASP#996)
could not drive `LibrarianPipeline`: it had the pipeline class but no way to produce
the components it takes. This module is that seam.

Everything here is construction only — no rows are read, no run is executed.
Callers that want a run use `queue_runner.run_librarian_queue`.

The DB and embedding imports are deliberately function-local. The rest of the
librarian package is hermetically testable precisely because it never imports the
database at module scope, and this is the one boundary where that stops being true;
keeping the imports inside the call preserves it for everyone else.

`load_config` now warns when `CRE_LIBRARIAN_TEMPERATURE` is still the 1.0 default,
because 1.0 is the identity transform — an uncalibrated softmax. Thresholding τ
against an uncalibrated confidence is not a smaller version of the right thing, it
is a different thing, so the run says so out loud rather than looking fine.
Module C's live entry point: knowledge_queue -> C.0..C.4 -> consumed. Deliberately
the same shape as Module B's `run_noise_filter` —
`(session, pipeline_run_id, ..., dry_run) -> RunSummary` with a `to_json()` the CLI
prints for the orchestrator to read. B reads harvest_input and fills
knowledge_queue; C drains knowledge_queue and stamps consumed_at.

What this adds over LibrarianPipeline is only the two DB-facing ends. The pipeline
stays persistence-free and hermetic; this wraps it with a live source on one side
and the write-back on the other.

Consumption is gated on persistence. The runner refuses to stamp anything unless it
was given a sink reporting `persists=True` and that sink accepted the batch. Graph
writes are still W8b; JsonlEnvelopeSink is what makes a live drain lossless in the
meantime.

Which rows get retired: everything the pipeline finished with — rows that linked,
rows that routed to review, and rows refused at the C.0 boundary, which is a
definitive refusal (re-reading a malformed row forever helps nobody). Rows that
*errored* mid-pipeline are left unconsumed, because those are the transient
failures — an embedding timeout, a cross-encoder hiccup — the next run should retry.
`RowOutcome` carries that distinction out of the pipeline, and `RunStats.errored`
keeps it separate from `skipped`.

UNCERTAIN rows are not touched. They are Module D's queue, per the B->C contract:
C reads `consumed_at IS NULL AND llm_label = 'KNOWLEDGE'`.

CLI: `--run_librarian` with `--run_id` drains the live queue; without it, walks a
JSONL fixture. `--librarian_envelopes_out` is required for a real run, because a
run with nowhere to put envelopes cannot safely retire anything. Combining
`--librarian_source` with `--run_id` is a usage error rather than one silently
winning.
Found while measuring this branch, and it is the same schema drift the rest of the
PR fixes — just on the harness side.

Golden rows are adapted into a synthetic knowledge_queue row before C.0 validates
them. When Module B's table shape moved in OWASP#989, `queue_row_from_golden` kept
minting the old flat row, so every one of the 319 rows was rejected at the
boundary:

    positive  0/292 (0%)   explicit  0/5 (0%)   ...

And the run still exited 0. With nothing validated, `explicit_total` stayed 0, the
`if explicit_total:` gate skipped itself, and a harness that graded absolutely
nothing reported success. That is precisely the skipped-gate-reports-success
failure the C.3 calibration gate was fixed for on OWASP#974.

Two changes:

- `queue_row_from_golden` mints the full v0.2 shape, with deterministic ids derived
  from the row id (the live reports key their shared audits off them, so two runs
  must produce the same ids).
- A boundary that rejects the whole dataset now fails the run with a message naming
  the likely cause, and an explicit slice that never reaches the resolver fails
  rather than vanishing.

Restored: 319/319 validate, explicit gate 5/5 PASS, hub-firewall strips 319 leaking
entries. Three tests cover it, including one that regresses the adapter to the flat
shape and asserts exit code 1.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@PRAteek-singHWY, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d32ff3b-49e7-43d6-b475-c608f95e8097

📥 Commits

Reviewing files that changed from the base of the PR and between 2f9004a and 63faf0b.

📒 Files selected for processing (8)
  • .github/workflows/librarian_regression.yml
  • application/tests/librarian/envelope_sink_test.py
  • application/tests/librarian/evaluate_harness_test.py
  • application/tests/librarian/queue_runner_test.py
  • application/utils/librarian/README.md
  • application/utils/librarian/queue_consumer.py
  • application/utils/librarian/queue_runner.py
  • docs/gsoc_2026_module_c/final_report.md

Summary by CodeRabbit

  • New Features

    • Added live Librarian queue processing through --run_id.
    • Added dry-run support, JSON summaries, and durable JSONL envelope output.
    • Added retry-safe queue handling and consumed-row tracking.
    • Added configurable Librarian temperature settings.
    • Added safety checks that can route uncertain items for review.
  • Documentation

    • Added Librarian usage documentation, runbook, final report, and evaluation metrics.
  • Bug Fixes

    • Improved validation and handling of malformed or unsupported knowledge-queue records.

Walkthrough

Changes

Queue contract and RFC adaptation

Layer / File(s) Summary
Queue contract and source adaptation
application/utils/librarian/schemas.py, application/utils/librarian/section_validator.py, application/tests/librarian/schemas_test.py, application/tests/librarian/section_validator_test.py, application/tests/librarian/fixtures/sample_knowledge_queue.jsonl
The queue-row model now matches Module B v0.2. Source metadata, locators, carried identities, heading paths, and validation errors use the expanded contract.

Live queue orchestration and persistence

Layer / File(s) Summary
Live queue processing
application/utils/librarian/{config_loader,envelope_sink,factory,knowledge_source,queue_consumer,queue_runner}.py, application/cmd/cre_main.py, cre.py
Live runs read scoped KNOWLEDGE rows, build components, write envelopes, mark completed rows consumed, and return JSON summaries. Dry runs avoid persistence and consumption.
Live processing validation
application/tests/librarian/{config_loader,envelope_sink,factory,knowledge_source,queue_consumer,queue_runner}_test.py
Tests cover configuration, component construction, source filtering, envelope output, consumption behavior, retry handling, sink requirements, ordering, filtering, and summary serialization.

Safety evaluation and row outcomes

Layer / File(s) Summary
Safety and outcome tracking
application/utils/librarian/{safety_guard,pipeline}.py, application/tests/librarian/{safety_guard,pipeline}_test.py
The pipeline evaluates safety before decisions, routes blocked rows to review, counts unevaluated verdicts, and records linked, review, skipped, and errored row outcomes.

Evaluation boundary and regression gate

Layer / File(s) Summary
Evaluation safeguards
scripts/evaluate_librarian.py, application/tests/librarian/evaluate_harness_test.py, .github/workflows/librarian_regression.yml
The harness generates deterministic schema-compatible queue rows and fails when boundary validation or resolver execution does not occur. The workflow runs the Librarian tests and golden-set gate.

Module C documentation and project metadata

Layer / File(s) Summary
Documentation and project metadata
application/utils/librarian/README.md, application/utils/librarian/__init__.py, docs/gsoc_2026_module_c/*, .gitignore
Documentation describes live processing, contracts, evaluation commands, persistence rules, integration status, and remaining implementation scope.

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.41% 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 identifies the Module C documentation, final metrics, and regression gate changes present in the pull request.
Description check ✅ Passed The description explains the Module C documentation, metrics, CI gate, known limitations, and verification steps related to 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 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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: 10

🧹 Nitpick comments (5)
application/tests/librarian/envelope_sink_test.py (1)

76-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Release the temporary directory and the schema file handles.

setUp creates a directory with tempfile.mkdtemp() and no tearDown removes it, so every test method leaves a directory behind. Line 137 also passes open(...) straight into json.load, so those file objects stay unclosed.

♻️ Proposed cleanup
     def setUp(self) -> None:
-        self.dir = tempfile.mkdtemp()
+        self._tmp = tempfile.TemporaryDirectory()
+        self.addCleanup(self._tmp.cleanup)
+        self.dir = self._tmp.name
         self.path = os.path.join(self.dir, "envelopes.jsonl")

And for the schema load at lines 136-140:

-        schemas = [
-            json.load(open(os.path.join(schema_dir, name), encoding="utf-8"))
-            for name in sorted(os.listdir(schema_dir))
-            if name.endswith(".json")
-        ]
+        schemas = []
+        for name in sorted(os.listdir(schema_dir)):
+            if not name.endswith(".json"):
+                continue
+            with open(os.path.join(schema_dir, name), encoding="utf-8") as fh:
+                schemas.append(json.load(fh))
🤖 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/envelope_sink_test.py` around lines 76 - 78,
Update the test fixture around setUp to add tearDown cleanup that removes the
temporary directory created by tempfile.mkdtemp(), and revise the schema-loading
code around json.load to use a context-managed file handle so it is closed after
reading. Preserve the existing test behavior and schema contents.
application/tests/librarian/factory_test.py (1)

94-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Isolate the environment in this test.

load_config() on line 106 reads the ambient environment. The other tests in this file clear it with mock.patch.dict(os.environ, {}, clear=True). If a developer or CI runner exports CRE_LIBRARIAN_RETRIEVER_BACKEND=pgvector, build_components takes the pgvector branch and calls can_use_pgvector_similarity on _FakeDatabase, which does not define it. The test then fails for an unrelated reason.

♻️ Proposed fix
-        with mock.patch(
-            "application.utils.librarian.cross_encoder." "build_cross_encoder_score_fn",
-            return_value=lambda pairs: [0.0 for _ in pairs],
-        ):
-            components = build_components(
-                _FakeDatabase(), config=load_config(), embed_fn=embed
-            )
+        with mock.patch(
+            "application.utils.librarian.cross_encoder." "build_cross_encoder_score_fn",
+            return_value=lambda pairs: [0.0 for _ in pairs],
+        ):
+            with mock.patch.dict(os.environ, {}, clear=True):
+                components = build_components(
+                    _FakeDatabase(), config=load_config(), embed_fn=embed
+                )
🤖 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/factory_test.py` around lines 94 - 109, Isolate
configuration loading in test_embed_fn_is_injectable_so_no_paid_call_is_made by
clearing the ambient environment with mock.patch.dict(os.environ, {},
clear=True) around load_config() and build_components. Preserve the existing
embed injection and assertion behavior while ensuring
CRE_LIBRARIAN_RETRIEVER_BACKEND cannot select the pgvector path.
application/utils/librarian/knowledge_source.py (1)

90-104: 🗄️ Data Integrity & Integration | 🔵 Trivial

Two concurrent drains of the same run can produce duplicate envelopes.

The read applies no row lock. Two runners that read the same pipeline_run_id at the same time select the same rows. mark_consumed filters on consumed_at IS NULL, so only one runner stamps each row, but both runners already wrote an envelope for it. The JSONL sink appends, so the file then holds two envelopes for one chunk_id.

The CLI is documented as manual and opt-in, so this is not reachable today. When the orchestrator drives this path, add with_for_update(skip_locked=True) on the read, or deduplicate envelopes by chunk_id in the consumer.

🤖 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/knowledge_source.py` around lines 90 - 104, The
_query method must prevent concurrent drains from selecting the same unconsumed
rows. Apply a row-level lock with skip_locked enabled to the KnowledgeQueueRow
query before ordering, limiting, and returning it, preserving the existing
run-id, label, and consumed-at filters.
application/utils/librarian/factory.py (1)

96-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider routing run_librarian through build_components to remove duplicated wiring.

application/cmd/cre_main.py lines 1160-1201 still construct the retriever and the reranker inline: the same pgvector guard, the same CandidatePool.from_mapping(cre_embeddings), the same build_retriever(...) arguments, and the same CrossEncoderReranker(...) arguments. build_components now performs that work and also exposes known_cre_ids, which is the known_ids set the fixture path needs. Two copies of this wiring will drift when a retriever or reranker argument changes in only one place.

The fixture path can call build_components(database, config=cfg) and read components.retriever, components.reranker, and components.known_cre_ids.

🤖 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/factory.py` around lines 96 - 148, Update
run_librarian in application/cmd/cre_main.py to call build_components(database,
config=cfg) instead of constructing the retriever and CrossEncoderReranker
inline. Reuse components.retriever, components.reranker, and
components.known_cre_ids for the fixture path, removing the duplicated pgvector
guard, embedding pool, and wiring arguments while preserving the existing
behavior.
cre.py (1)

334-340: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider the symmetric check for --librarian_envelopes_out.

This check rejects --librarian_source with --run_id, because the fixture path would be silently ignored. The mirror case is not checked: --librarian_envelopes_out without --run_id runs the fixture walk-through, which writes no envelopes, so the flag is silently ignored in the same way.

♻️ Proposed additional check
     if args.librarian_source and args.run_id.strip():
         parser.error(
             "--librarian_source reads a fixture and cannot be combined "
             "with --run_id (which drains the live knowledge_queue)"
         )
+    if args.librarian_envelopes_out and not args.run_id.strip():
+        parser.error(
+            "--librarian_envelopes_out only applies to the live queue path; "
+            "add --run_id <pipeline_run_id>"
+        )
🤖 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 `@cre.py` around lines 334 - 340, Extend the argument validation near the
existing librarian_source/run_id check to reject --librarian_envelopes_out when
--run_id is absent. Ensure the error clearly states that envelope output
requires the live knowledge_queue path, while preserving the existing validation
and fixture walk-through behavior otherwise.
🤖 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 @.github/workflows/librarian_regression.yml:
- Around line 37-38: Update the actions/checkout@v4 step in the “Check out code”
workflow action to set persist-credentials to false, preventing the checkout
from storing the GITHUB_TOKEN in local Git configuration.

In `@application/utils/librarian/queue_runner.py`:
- Around line 66-73: Update the run summary flow around run_librarian_queue and
QueueSummary so status is derived from the completed counts before to_json
serializes it: mark runs with errored > 0 or safety_unevaluated > 0 as
degraded/non-OK, while keeping status "ok" only when both counts are zero.

In `@application/utils/librarian/README.md`:
- Around line 9-12: Update the opening fences for both ASCII diagrams to use the
text language tag: application/utils/librarian/README.md lines 9-12 and
docs/gsoc_2026_module_c/final_report.md lines 21-27 should each use ```text,
with the diagram contents unchanged.
- Around line 45-56: Update LibrarianPipeline and queue_runner handling for C.0
rejections so a skipped RowOutcome is durably persisted before consumed_at is
set; if persistence is unavailable or fails, leave the row unconsumed. Preserve
the distinction between rejected rows and mid-pipeline errors in RunStats.

In `@docs/gsoc_2026_module_c/final_metrics.md`:
- Around line 119-125: Update the Week 9 selective-reranking comparison around
the reported 250/319 and shipped 238 results to explicitly define the evaluation
slice, metric, and denominator for both values. Either label the differing
populations clearly or recompute the baseline on the same 319-row population
before retaining the conclusion about the reranker versus corpus quality.
- Around line 3-4: Revise the provenance statement in final_metrics.md so it
applies only to the reproducible result tables generated by the command shown
below, rather than claiming every number in the document is command-derived.
Preserve the historical experiment and corpus-observation sections, and add
their provenance only if it is already known.

In `@docs/gsoc_2026_module_c/final_report.md`:
- Line 51: Use “no external database service” consistently in
docs/gsoc_2026_module_c/final_report.md lines 51-51 and replace “no DB” with “no
external DB service” in docs/gsoc_2026_module_c/runbook.md lines 168-178, while
retaining the documented local SQLAlchemy sessions and row inserts.

In `@docs/gsoc_2026_module_c/runbook.md`:
- Around line 46-48: Revise the C.3 exit-status statement in the runbook to
explicitly limit it to live evaluation. Preserve the existing explanation that
failed or skipped ECE causes live runs to return non-zero, while clarifying that
hermetic explicit-gate and boundary failures are also handled by the harness and
that C.4 remains informational.
- Around line 150-152: Update the troubleshooting section headed “A real run
reports rows decided but nothing consumed” to describe only sink refusal after
rows were processed. Add a separate preflight-error entry explaining that
application/cmd/cre_main.py raises SystemExit before processing when a non-dry
run omits --librarian_envelopes_out, so it cannot report “rows decided.”

In `@scripts/evaluate_librarian.py`:
- Around line 543-550: Update the evaluation flow around the rows selection and
the validated_total check so an empty rows result immediately fails before
reporting success, covering both an empty --dataset and a non-matching --slice.
Preserve the existing C.0 validation behavior for non-empty selections, and add
regression tests for both empty-selection cases.

---

Nitpick comments:
In `@application/tests/librarian/envelope_sink_test.py`:
- Around line 76-78: Update the test fixture around setUp to add tearDown
cleanup that removes the temporary directory created by tempfile.mkdtemp(), and
revise the schema-loading code around json.load to use a context-managed file
handle so it is closed after reading. Preserve the existing test behavior and
schema contents.

In `@application/tests/librarian/factory_test.py`:
- Around line 94-109: Isolate configuration loading in
test_embed_fn_is_injectable_so_no_paid_call_is_made by clearing the ambient
environment with mock.patch.dict(os.environ, {}, clear=True) around
load_config() and build_components. Preserve the existing embed injection and
assertion behavior while ensuring CRE_LIBRARIAN_RETRIEVER_BACKEND cannot select
the pgvector path.

In `@application/utils/librarian/factory.py`:
- Around line 96-148: Update run_librarian in application/cmd/cre_main.py to
call build_components(database, config=cfg) instead of constructing the
retriever and CrossEncoderReranker inline. Reuse components.retriever,
components.reranker, and components.known_cre_ids for the fixture path, removing
the duplicated pgvector guard, embedding pool, and wiring arguments while
preserving the existing behavior.

In `@application/utils/librarian/knowledge_source.py`:
- Around line 90-104: The _query method must prevent concurrent drains from
selecting the same unconsumed rows. Apply a row-level lock with skip_locked
enabled to the KnowledgeQueueRow query before ordering, limiting, and returning
it, preserving the existing run-id, label, and consumed-at filters.

In `@cre.py`:
- Around line 334-340: Extend the argument validation near the existing
librarian_source/run_id check to reject --librarian_envelopes_out when --run_id
is absent. Ensure the error clearly states that envelope output requires the
live knowledge_queue path, while preserving the existing validation and fixture
walk-through behavior otherwise.
🪄 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: c525282c-aa23-4d78-b0f3-fbff5fee39aa

📥 Commits

Reviewing files that changed from the base of the PR and between 71f6c81 and 2f9004a.

📒 Files selected for processing (32)
  • .github/workflows/librarian_regression.yml
  • .gitignore
  • application/cmd/cre_main.py
  • application/tests/librarian/config_loader_test.py
  • application/tests/librarian/envelope_sink_test.py
  • application/tests/librarian/evaluate_harness_test.py
  • application/tests/librarian/factory_test.py
  • application/tests/librarian/fixtures/sample_knowledge_queue.jsonl
  • application/tests/librarian/knowledge_source_test.py
  • application/tests/librarian/pipeline_test.py
  • application/tests/librarian/queue_consumer_test.py
  • application/tests/librarian/queue_runner_test.py
  • application/tests/librarian/safety_guard_test.py
  • application/tests/librarian/schemas_test.py
  • application/tests/librarian/section_validator_test.py
  • application/utils/librarian/README.md
  • application/utils/librarian/__init__.py
  • application/utils/librarian/config_loader.py
  • application/utils/librarian/envelope_sink.py
  • application/utils/librarian/factory.py
  • application/utils/librarian/knowledge_source.py
  • application/utils/librarian/pipeline.py
  • application/utils/librarian/queue_consumer.py
  • application/utils/librarian/queue_runner.py
  • application/utils/librarian/safety_guard.py
  • application/utils/librarian/schemas.py
  • application/utils/librarian/section_validator.py
  • cre.py
  • docs/gsoc_2026_module_c/final_metrics.md
  • docs/gsoc_2026_module_c/final_report.md
  • docs/gsoc_2026_module_c/runbook.md
  • scripts/evaluate_librarian.py

Comment thread .github/workflows/librarian_regression.yml
Comment thread application/utils/librarian/queue_runner.py
Comment thread application/utils/librarian/README.md Outdated
Comment on lines +45 to +56
**1. A row is only retired if its envelope survived.** Marking a queue row
consumed tells Module B never to offer that chunk again. Doing that while the
envelope goes nowhere destroys the chunk outright. So `queue_runner` refuses to
stamp anything unless it was given a sink that reports `persists=True` *and* that
sink accepted the batch. Dry runs use `NullEnvelopeSink`, which reports `False`,
so a dry run can never consume.

**2. Errors and refusals are different.** A row rejected at the C.0 boundary is
consumed — re-reading a malformed row forever helps nobody. A row that *errored*
mid-pipeline (an embedding timeout, a cross-encoder hiccup) is left unconsumed,
because the next run should retry it. `RunStats` counts them separately on
purpose.

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Expect: C.0 rejections have a durable outcome before consumed_at.
rg -n -C 8 \
  '\b(consumed_at|rejected|skipped|errored|persists|envelope)\b' \
  application/utils/librarian application/tests/librarian

Repository: OWASP/OpenCRE

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- queue_runner structure ---'
ast-grep outline application/utils/librarian/queue_runner.py
printf '%s\n' '--- queue_runner implementation ---'
sed -n '1,280p' application/utils/librarian/queue_runner.py
printf '%s\n' '--- C.0 validator exceptions and pipeline handling ---'
rg -n -C 10 'SectionValidationError|section_from_queue|section_from_knowledge|skipped|consumed' \
  application/utils/librarian/pipeline.py \
  application/utils/librarian/section_validator.py \
  application/utils/librarian/queue_runner.py

Repository: OWASP/OpenCRE

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- pipeline result and retirement selection ---'
sed -n '120,155p' application/utils/librarian/pipeline.py
sed -n '245,285p' application/utils/librarian/pipeline.py
printf '%s\n' '--- queue consumer ---'
cat -n application/utils/librarian/queue_consumer.py
printf '%s\n' '--- focused retirement tests ---'
sed -n '245,285p' application/tests/librarian/queue_runner_test.py
printf '%s\n' '--- focused behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path

pipeline = Path("application/utils/librarian/pipeline.py").read_text()
runner = Path("application/utils/librarian/queue_runner.py").read_text()

checks = {
    "C.0 rejection emits no envelope": "outcomes.append(RowOutcome(row_id, None, RowStatus.skipped))" in pipeline
        and "continue" in pipeline[pipeline.index("except SectionValidationError"):pipeline.index("except SectionValidationError") + 300],
    "retirement uses finished row ids": "finished = result.finished_row_ids()" in runner
        and "mark_consumed(session, finished" in runner,
    "sink writes only pipeline envelopes": "summary.persisted = sink.write(result.envelopes)" in runner,
}
for name, passed in checks.items():
    print(f"{name}: {'YES' if passed else 'NO'}")
PY

Repository: OWASP/OpenCRE

Length of output: 6991


Persist C.0 rejection outcomes before retiring rows.

LibrarianPipeline records C.0 rejections only as RowOutcome(status=skipped) and emits no envelope. queue_runner then retires those rows. Emit a durable rejection outcome before consumed_at, or leave the row unconsumed.

🤖 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/README.md` around lines 45 - 56, Update
LibrarianPipeline and queue_runner handling for C.0 rejections so a skipped
RowOutcome is durably persisted before consumed_at is set; if persistence is
unavailable or fails, leave the row unconsumed. Preserve the distinction between
rejected rows and mid-pipeline errors in RunStats.

Comment on lines +3 to +4
Every number here comes from one command over the committed golden set. Nothing
is hand-copied from a notebook.

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Limit the one-command provenance claim.

Line [3] says every number in this file comes from the command at Lines [6]-[10]. Lines [108]-[125] also report historical experiments and corpus observations. Add provenance for those values, or limit the statement to the reproducible result tables.

Proposed wording
-Every number here comes from one command over the committed golden set. Nothing
-is hand-copied from a notebook.
+The result tables below come from one command over the committed golden set.
+Historical experiments and corpus observations are identified in their sections.
📝 Committable suggestion

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

Suggested change
Every number here comes from one command over the committed golden set. Nothing
is hand-copied from a notebook.
The result tables below come from one command over the committed golden set.
Historical experiments and corpus observations are identified in their sections.
🤖 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 `@docs/gsoc_2026_module_c/final_metrics.md` around lines 3 - 4, Revise the
provenance statement in final_metrics.md so it applies only to the reproducible
result tables generated by the command shown below, rather than claiming every
number in the document is command-derived. Preserve the historical experiment
and corpus-observation sections, and add their provenance only if it is already
known.

Comment on lines +119 to +125
Week 9's selective-reranking experiment — gating C.2 to fire only where it helps
— reached 250/319 top-1 against the shipped 238, holding precision at τ=0.80. It
is deliberately not shipped: it needs two calibrators and held-out validation
that there was not time to do honestly.

**The path to ≥0.90 runs through populating CRE descriptions, not through a
better reranker.** That is a corpus problem and belongs upstream of Module C.

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 | 🟡 Minor | ⚡ Quick win

Define the slice for the selective-reranking comparison.

Line [60] reports C.2 on 292 positive rows. Lines [119]-[120] report 250/319 versus 238, but do not define the metric or slice for either value. Label both denominators and metrics, or compare the baseline on the same population before drawing the conclusion at Lines [124]-[125].

🤖 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 `@docs/gsoc_2026_module_c/final_metrics.md` around lines 119 - 125, Update the
Week 9 selective-reranking comparison around the reported 250/319 and shipped
238 results to explicitly define the evaluation slice, metric, and denominator
for both values. Either label the differing populations clearly or recompute the
baseline on the same 319-row population before retaining the conclusion about
the reranker versus corpus quality.

| 7 | — | *analysis* | Auto-link threshold sweep; τ held at 0.80 |
| 8 | — | *this PR* | Live B→C integration — queue drain, sink, write-back |

**223 tests**, all hermetic — no database, API key, or model download required.

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use “no external database service” consistently.

docs/gsoc_2026_module_c/final_report.md and docs/gsoc_2026_module_c/runbook.md describe the test suite as database-free, while docs/gsoc_2026_module_c/runbook.md documents real SQLAlchemy sessions and row inserts.

  • docs/gsoc_2026_module_c/final_report.md#L51-L51: replace “no database” with “no external database service”.
  • docs/gsoc_2026_module_c/runbook.md#L168-L178: replace “no DB” with “no external DB service” and retain the local SQLAlchemy test detail.
📍 Affects 2 files
  • docs/gsoc_2026_module_c/final_report.md#L51-L51 (this comment)
  • docs/gsoc_2026_module_c/runbook.md#L168-L178
🤖 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 `@docs/gsoc_2026_module_c/final_report.md` at line 51, Use “no external
database service” consistently in docs/gsoc_2026_module_c/final_report.md lines
51-51 and replace “no DB” with “no external DB service” in
docs/gsoc_2026_module_c/runbook.md lines 168-178, while retaining the documented
local SQLAlchemy sessions and row inserts.

Comment on lines +46 to +48
**Only the C.3 gate sets the exit status.** A failed *or skipped* ECE gate
returns non-zero, so a live run cannot pass without calibration having actually
run. C.4 is informational.

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the C.3 exit-status statement.

Lines [23]-[25] state that the hermetic harness also fails for explicit-gate and boundary failures. Line [46] says only C.3 sets the exit status. Scope this statement to live evaluation so operators do not misread the other gates.

Proposed wording
-**Only the C.3 gate sets the exit status.**
+**For live evaluation, C.3 adds the calibration exit-status gate.**
📝 Committable suggestion

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

Suggested change
**Only the C.3 gate sets the exit status.** A failed *or skipped* ECE gate
returns non-zero, so a live run cannot pass without calibration having actually
run. C.4 is informational.
**For live evaluation, C.3 adds the calibration exit-status gate.** A failed *or skipped* ECE gate
returns non-zero, so a live run cannot pass without calibration having actually
run. C.4 is informational.
🤖 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 `@docs/gsoc_2026_module_c/runbook.md` around lines 46 - 48, Revise the C.3
exit-status statement in the runbook to explicitly limit it to live evaluation.
Preserve the existing explanation that failed or skipped ECE causes live runs to
return non-zero, while clarifying that hermetic explicit-gate and boundary
failures are also handled by the harness and that C.4 remains informational.

Comment on lines +150 to +152
**A real run reports rows decided but nothing consumed**
The sink refused the batch, or you passed no `--librarian_envelopes_out`.
Consumption is gated on persistence — this is the safe failure, not a bug.

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Separate the preflight error from a sink refusal.

The supplied application/cmd/cre_main.py entrypoint raises SystemExit before processing when a non-dry run omits --librarian_envelopes_out. Therefore that case cannot report “rows decided.” Document the preflight error separately from a sink that refuses an already-processed batch.

Proposed troubleshooting split
 **A real run reports rows decided but nothing consumed**
-The sink refused the batch, or you passed no `--librarian_envelopes_out`.
+The sink refused the batch after processing. Check sink persistence and logs.
+
+**A real run fails before processing**
+If a non-dry run omits `--librarian_envelopes_out`, the CLI raises before
+the queue runner starts.
📝 Committable suggestion

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

Suggested change
**A real run reports rows decided but nothing consumed**
The sink refused the batch, or you passed no `--librarian_envelopes_out`.
Consumption is gated on persistence — this is the safe failure, not a bug.
**A real run reports rows decided but nothing consumed**
The sink refused the batch after processing. Check sink persistence and logs.
**A real run fails before processing**
If a non-dry run omits `--librarian_envelopes_out`, the CLI raises before
the queue runner starts.
🤖 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 `@docs/gsoc_2026_module_c/runbook.md` around lines 150 - 152, Update the
troubleshooting section headed “A real run reports rows decided but nothing
consumed” to describe only sink refusal after rows were processed. Add a
separate preflight-error entry explaining that application/cmd/cre_main.py
raises SystemExit before processing when a non-dry run omits
--librarian_envelopes_out, so it cannot report “rows decided.”

Comment on lines +543 to +550
if rows and not validated_total:
print(
f"validation (C.0): 0/{len(rows)} rows validated — the whole golden "
"set was rejected at the boundary, so no report below this line "
"graded anything; FAILED (gates did not run). Most likely the "
"synthetic row shape has drifted from Module B's knowledge_queue"
)
return 1

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail empty selections before reporting success.

If --dataset is empty, or --slice matches no rows, rows is empty. Line 543 bypasses this failure path. The command then returns 0 although C.0 and C.0.5 did not run.

Reject an empty selection before the total-validation check. Add regression coverage for an empty dataset and a non-matching slice.

Proposed fix
     validated_total = sum(validated_per_slice.values())
-    if rows and not validated_total:
+    if not rows:
+        print("validation (C.0): 0 rows selected; FAILED (gates did not run)")
+        return 1
+    if not validated_total:
📝 Committable suggestion

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

Suggested change
if rows and not validated_total:
print(
f"validation (C.0): 0/{len(rows)} rows validated — the whole golden "
"set was rejected at the boundary, so no report below this line "
"graded anything; FAILED (gates did not run). Most likely the "
"synthetic row shape has drifted from Module B's knowledge_queue"
)
return 1
validated_total = sum(validated_per_slice.values())
if not rows:
print("validation (C.0): 0 rows selected; FAILED (gates did not run)")
return 1
if not validated_total:
print(
f"validation (C.0): 0/{len(rows)} rows validated — the whole golden "
"set was rejected at the boundary, so no report below this line "
"graded anything; FAILED (gates did not run). Most likely the "
"synthetic row shape has drifted from Module B's knowledge_queue"
)
return 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 `@scripts/evaluate_librarian.py` around lines 543 - 550, Update the evaluation
flow around the rows selection and the validated_total check so an empty rows
result immediately fails before reporting success, covering both an empty
--dataset and a non-matching --slice. Preserve the existing C.0 validation
behavior for non-empty selections, and add regression tests for both
empty-selection cases.

…run status

**`consumed_at` was written timezone-aware into a naive column.** The runner
passes an aware `at`, and `consumed_at` is a plain `DateTime`. That is
dialect-dependent — SQLite keeps the offset in the string, Postgres drops it —
and the B->C contract is explicit that the UTC wall clock is "stored and read
back timezone-naive". `mark_consumed` now converts to UTC and strips tzinfo, so
the stored instant is correct under both and matches the `created_at` values B
writes beside it. Only ever tested on SQLite, which is exactly why this was
invisible.

**`RunSummary.status` was the constant "ok".** The orchestrator reads this JSON,
so a run that dropped rows to errors, or decided them without the safety path,
still reported success in the one field a consumer branches on. That is the same
failure this module keeps fixing elsewhere: a field that looks like a verdict
while measuring nothing. `finalize_status()` now derives it from the counts and
names the reason.

Note this makes every real run today report `degraded: N decided without the
safety path`, because `NullSafetyGuard` is what ships and no row is ever
evaluated. That is accurate rather than noisy — C genuinely runs without a
safety path until the detector lands, and W8b's graph writer is required to
refuse on exactly this signal.

Also:
- `envelope_sink_test` removes its temp directory; each method wrote envelopes
  containing chunk text and left the directory behind.
- the harness contract test validates every golden row rather than the first 25;
  `main()` validates the whole selection, so a row past the cap could fail C.0
  while the test still passed.
…the regression gate

The last of the Module C plan that is not code: the package README, a runbook, the
measured final metrics, the GSoC final report, and the CI gate that keeps the
golden-set numbers from rotting.

`.gitignore` blanket-ignores `*.md`, so the new docs need explicit negations —
the same pattern `docs/Mid_eval_blog_gsoc2026/module_B_mideval_blog.md` already
uses. Nothing else is un-ignored; gsoc-notes and other local markdown stay out.

The regression workflow is deliberately hermetic: no DB, no key, no model
download. The semantic reports (C.1 recall, C.2 top-1, the C.3 ECE gate, C.4
decision accuracy) need live CRE vectors, and seeding a candidate pool from golden
text offline is exactly the leakage the hub firewall exists to strip — a CI job
that "measured" them would be measuring nothing. What it does catch is the failure
that actually bit this branch: a Module B schema change silently rejecting every
row at the C.0 boundary.

`final_metrics.md` records the numbers as measured, including the two that were
missed. Top-1 is 75% against a planned 90%, and the write-up says why rather than
rounding it away: W8's investigation tried thirteen reranker levers and all
thirteen regressed, because 427 of 428 CREs have empty descriptions — a
cross-encoder cannot cross-attend to a bare title. C.1 still reaches 98% recall
over that same corpus, which localises the problem precisely. The path to 90% runs
through populating CRE descriptions, not through a better reranker, and that is a
corpus problem upstream of Module C.

W9's selective reranking (250/319 vs the shipped 238) is documented as measured
but deliberately unshipped: it needs two calibrators and held-out validation.
Shipping it on in-sample numbers would be the same greenwashing this module spent
four separate fixes removing.
The job failed on its first run: `virtualenv: not found`. `make install-python`
builds a venv — which needs an apt `virtualenv` the other workflows install
first — and then runs `playwright install`. Neither is used by a hermetic
librarian run, and worse, the venv is never activated by the steps that follow,
so they would have executed against a bare interpreter even had it built.

Installs `requirements-dev.txt` straight into the runner's interpreter instead,
which is what the two steps actually need, and drops the browser download.

Also switched the unit step from pytest to `unittest discover`, matching how
`make test` runs the suite in the existing Test job.
…gram fences

- `actions/checkout` now sets `persist-credentials: false`. The job only reads
  the tree, so leaving the token in `.git/config` would expose it to anything
  the test run executes.
- Tagged both ASCII pipeline diagrams as ```text (markdownlint MD040).
@PRAteek-singHWY

Copy link
Copy Markdown
Contributor Author

Folding this into #1011 rather than reviewing it as a stack.

The split was mine and it was not worth the extra review round: this is one week's work, and separating the integration from its own docs meant two PRs, two CI runs and two review cycles for a change that reads better in one place. #1011 now carries all 11 commits — the live B→C integration plus the package README, runbook, final metrics, final report, and the regression-gate workflow.

Nothing is dropped; #1011 is a strict superset of what was here. Closing.

Sorry for the churn, @northdpole — please review #1011 only.

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