# Plan: CLO/MLO Bloom grounding for the assurance inventory

The assurance audit currently reads assessment **titles** only. On a course with generic titles ("Module 3 Homework"), `classify_blooms` returns Unknown for every item, so the Bloom distribution, counterfeit detection, AI-Forward D2/D4, and bloom_calibration_findings all go blind (verified live on STP530 256162: 0/24 titles carried a Bloom verb).

The real objectives live where the user pointed: **CLOs in the syllabus** (endpoint, or attached docx/pdf), **MLOs in module overview pages**. Wire those in.

## Decisions (user)
- **Wire into the inventory (shared)** so every consumer benefits.
- **Fill-Unknown only**: assessment title verb wins when present; module MLO Bloom fills only when the title is Unknown.

## Net-new

### 1. `scripts/objective_extractor.py` (new, reusable by alignment too)
- `extract_course_clos(config) -> {"clos": [str], "bloom_index": int, "bloom_level": str}`
  Syllabus endpoint: find the "Learning Outcomes / students will be able to:" list, split into CLO statements, Bloom-classify each, course bloom = max. (docx/pdf attachment parsing is a later path; endpoint is what STP530 and the QM template use. ponytail: skip attachment parsing until a course needs it.)
- `extract_module_mlos(config, tree=None) -> {module_name: {"bloom_index","bloom_level","mlos":[str]}}`
  Per module, find the overview Page (title contains overview/objectives/start here, else first Page item), fetch body, regex `MLO d+.d+ <verb> ...`, Bloom-classify each, module bloom = max.
- Graceful degrade: any fetch failure -> empty result; the inventory simply gets no MLO fill (same as today). Throughput-charged.

### 2. `scripts/assurance_inventory.py`
- **Fix module mapping for quizzes** (existing bug): line 626 looks up `module_map.get(aid)` by assignment-id, but module-item `content_id` for a quiz is the **quiz_id**, so every quiz maps to None. Add a quiz_id fallback: `module_map.get(int(assignment["quiz_id"]))`.
- Build `module_mlos` + `course_clos` once per course.
- Attach to each record: `bloom_level`, `bloom_index`, `bloom_source` ("title" | "mlo" | "unknown"). Fill-Unknown: title verb first, module MLO fallback by the record's (now reliable) module name.
- Add `course_clos` and `module_mlos` to the inventory output. Note new per-course API calls in assumptions.

### 3. `scripts/assurance_audit.py` (consume, do not recompute)
- Helper `_rec_bloom(rec) -> (verb, level, source)`: returns `rec["bloom_level"]`/`bloom_source` when set, else `classify_blooms(rec["title"])` with source "title"/"unknown". Back-compatible: raw records without the field fall through to the existing title path (existing tests unchanged).
- Route every `classify_blooms(title)` call through `_rec_bloom`: cognitive demand, counterfeit, AI-Forward D2, `_package_ai_items` counterfeit, `bloom_calibration_findings`.
- **Honesty guard on counterfeit**: deterministic `counterfeit_objective` fires ONLY when `bloom_source != "mlo"` (the quiz's *own* title claims Analyze+). When an objective quiz is MLO-grounded to Apply+, do NOT deterministically brand it counterfeit; instead emit a `needs_ai_review` item `judgment_type="counterfeit_objective_mlo"` asking whether the quiz is the MLO's assessment of record or a legitimate low-stakes concept check. (A concept check in a Create-level module is legitimate, not counterfeit.)
- bloom_calibration_findings benefit directly and correctly: an MLO-grounded "Module 1 Homework" -> Create -> `create_oneshot` fires when there's no process trail. This is the real substitutability insight and the homework IS the MLO's assessment of record, so deterministic is appropriate here.

## Scorecard / inventory additions
- inventory: `course_clos`, `module_mlos`; per-record `bloom_level`/`bloom_index`/`bloom_source`.
- scorecard: `dimensions.C` gains `bloom_source_breakdown` (counts by title/mlo/unknown) so the user can see how much grounding came from MLOs vs titles. New needs_ai_review type `counterfeit_objective_mlo`.

## Out of scope
- docx/pdf syllabus attachment parsing (endpoint only for now; log if a syllabus is attachment-only and CLOs weren't found).
- Alignment scoring (CLO/MLO->assessment coverage) stays in the `alignment` skill; this PR only exposes the data.

## Release
feat -> minor: 7.41.0 -> 7.42.0. VERSION + both manifests + tag v7.42.0 + CHANGELOG. Tests extend test_assurance_inventory + test_assurance_audit + new test_objective_extractor.

## Verification
- Unit: CLO/MLO regex + bloom classification on captured STP530 syllabus + overview HTML fixtures.
- Live: STP530 256162 should now show Bloom distribution dominated by Apply/Create (not 100% Unknown), quiz module attribution non-None, and bloom_source_breakdown showing mlo-fill. Deterministic tier unchanged by MLO grounding (counterfeit stays title-based; MLO counterfeit is AI-review only).
