# ARS v3.6.5 — Material Passport `literature_corpus[]` Consumer Integration

**Status:** Design — pending implementation plan
**Author:** brainstormed 2026-04-26
**Target release:** v3.6.5 (suite invariant; patch-level)
**Predecessor:** v3.6.4 (input port shipped 2026-04-25, main `aa96fa6`)
**Repository:** Imbad0202/academic-research-skills

---

## 1. Goal & Non-Goals

### 1.1 Goal

Deliver the consumer side of the `literature_corpus[]` input port shipped in v3.6.4. When a user-written adapter has populated a Material Passport with a non-empty `literature_corpus[]`, the two Phase 1 literature agents (`deep-research/agents/bibliography_agent.md` and `academic-paper/agents/literature_strategist_agent.md`) shall enter a **corpus-first, search-fills-gap** flow and report corpus utilisation transparently in PRISMA-style search documentation.

### 1.2 Non-Goals (out of v3.6.5 scope)

- No schema change. `literature_corpus_entry.schema.json`, `rejection_log.schema.json`, and Schema 9 structural fields all remain frozen at v3.6.4 shape.
- `citation_compliance_agent` (Phase 5a) does not read corpus in v3.6.5. Deferred to v3.6.6+.
- `source_verification_agent` does not use corpus to cross-check sources in v3.6.5. Deferred to v3.6.6+.
- No new environment flag. No new mode. No mode-scoped opt-in.
- No deterministic behaviour test. v3.6.5 is prompt-layer; behaviour validation is reserved for the first real systematic-review run after ship.
- No change to `academic-paper-reviewer` suite.

### 1.3 Scope summary

- 2 agent.md files modified (`bibliography_agent` + `literature_strategist_agent`)
- 1 new reference document (`academic-pipeline/references/literature_corpus_consumers.md`)
- 1 new CI lint script (`scripts/check_corpus_consumer_protocol.py`) + unit tests
- 1 new manifest (`scripts/corpus_consumer_manifest.json`)
- `shared/handoff_schemas.md` Schema 9 paragraph: retire `Consumer-side integration deferred to v3.6.5+` caveat (PR-B only)
- CI wired through `.github/workflows/spec-consistency.yml`
- 7-touchpoint version sweep (PR-B only)
- Two-PR rollout: PR-A is single-consumer pre-release (one live consumer, no version bump, no release artifacts); PR-B is the release sweep, carrying the v3.6.5 tag.

---

## 2. Architecture

### 2.1 Component map

```
[ User adapter (out-of-tree) ]
        │ writes
        ▼
   passport.yaml  ──── (optional) literature_corpus[] entries
        │ schema-validated by
        ▼
   scripts/check_literature_corpus_schema.py  (existing, v3.6.4)
        │ passport flows downstream via Material Passport
        ▼
┌─────────────────────────────────────────────────────────┐
│ Phase 1 consumer agents (NEW reading behaviour, v3.6.5) │
│                                                         │
│  • deep-research/agents/bibliography_agent.md           │
│  • academic-paper/agents/literature_strategist_agent.md │
│                                                         │
│  Both agents:                                           │
│    1. Detect non-empty literature_corpus[] (presence)   │
│    2. Pre-screen against current RQ (corpus-first)      │
│    3. Honest reporting (skip / 0-hit surface)           │
│    4. Emit PRE-SCREENED block in Search Strategy Report │
│    5. Annotated bib stays neutral — no source tag       │
└─────────────────────────────────────────────────────────┘
        │ shared reading contract
        ▼
   academic-pipeline/references/literature_corpus_consumers.md  (NEW)
        │ structure enforced by
        ▼
   scripts/check_corpus_consumer_protocol.py  (NEW lint)
        │ wired into
        ▼
   .github/workflows/spec-consistency.yml (CI gate)
```

### 2.2 Architectural invariants

1. **Schema invariance.** v3.6.5 does not modify any JSON schema or Schema 9 structural field. All metadata in the PRE-SCREENED block is aggregated from existing entry fields (`obtained_via`, `obtained_at`, `adapter_name`, `adapter_version`, `citation_key`).
2. **Reading-only consumer boundary.** Consumer agents only read `literature_corpus[]`. They never modify, backfill, or derive new passport fields. Writing to the passport `literature_corpus[]` remains the sole responsibility of user adapters per v3.6.4 input port boundary.
3. **Attribution split.** Reproducibility records (which entries came from corpus, hit rate, skipped entries) live only in the Search Strategy Report's `PRE-SCREENED FROM USER CORPUS:` block. The annotated bibliography stays neutral with no source-attribution tags.

### 2.3 Cross-skill backpointer convention

Both consumer agent files carry a backpointer to `academic-pipeline/references/literature_corpus_consumers.md`. Pattern follows the v3.4.0 compliance agent's cross-skill backpointer (compliance agent in `shared/agents/` referenced from `academic-paper/references/anti_leakage_protocol.md`). Rationale: the consumer protocol is owned at the pipeline layer (mirroring v3.6.4's adapter contract location), but the actual reading behaviour lives in skill-specific agents; backpointers are the minimal coupling.

### 2.4 Out-of-scope coupling points (explicitly deferred)

- `citation_compliance_agent` Phase 5a corpus integration → v3.6.6+
- `source_verification_agent` corpus-based cross-checking → v3.6.6+
- The `literature_corpus_consumers.md` second consumer block is a stub during the PR-A → PR-B window (see §6 Rollout).

### 2.5 Architectural risks

1. **Skeleton-vs-stub clarity.** The `literature_corpus_consumers.md` skeleton during PR-A must contain an explicit, lint-detectable stub marker for the academic-paper consumer block (`Status: Stub — implementation in PR-B (v3.6.5)`). Without this, the file could be misread as already covering both consumers.
2. **Prose-template drift.** The PRE-SCREENED block structure must be pinned via a markdown example block in agent.md (not described in prose only). LLMs improvising structure across model versions will drift; the lint enforces the example block exists.
3. **Backpointer drift.** The two agent backpointers point at one file; renaming or moving the reference doc requires updating both call sites. The lint enforces the backpointer string is present in each agent file listed in the manifest.

---

## 3. Data Flow

### 3.1 Shared corpus-first flow

`bibliography_agent` and `literature_strategist_agent` both follow the same five-step reading flow. Skill-specific deliverables (PRISMA documentation in deep-research; literature matrix and gap identification in academic-paper) are layered on top of the merged `final_included` set without altering the flow itself.

```
Step 0: Detect literature_corpus[] presence and minimal shape
    The agent applies a MINIMAL SHAPE CHECK on the corpus before reading
    further. This is not JSON Schema validation (Iron Rule 4 forbids that).
    It checks only what the consumer needs to read each entry safely
    in Steps 1–4 — the v3.6.4 required fields:
      shape OK ≡ literature_corpus is a YAML list AND
                 each entry is a YAML mapping AND
                 each entry has the five v3.6.4 required fields
                 in readable form:
                   - citation_key: non-empty string
                   - title: non-empty string
                   - authors: non-empty list
                   - year: integer or numeric-coercible value
                   - source_pointer: non-empty string

    if passport lacks literature_corpus field OR literature_corpus == []:
        → original external-DB-only flow (no behaviour change vs v3.6.4)
    elif YAML parse of passport fails OR shape check fails:
        → emit [CORPUS PARSE FAILURE: <one-line cause>] in the Search
          Strategy Report; fall back to external-DB-only flow
          (see §4.1 F2 and §4.4 Iron Rule 4)
    else:
        → enter corpus-first flow (Steps 1–4)
```

The shape check is "presence + readable form" only. It does not enforce JSON Schema constraints such as `citation_key` regex pattern, `authors` CSL-JSON shape, `year` 4-digit range, or `source_pointer` URI format — those remain Iron Rule 4's "do not re-validate" territory. The shape check guarantees Step 1 can read each entry's required fields without crashing on missing keys or wrong types; deeper semantic problems become F1 skip cases as before.

```
Step 1: Pre-screen corpus against current RQ
    for each entry in literature_corpus[]:
        a. read required fields (always present per v3.6.4 schema):
             citation_key, title, authors, year, source_pointer
        b. read optional fields (use only if present; absence is normal):
             venue, doi, tags, abstract, user_notes
        c. apply current Inclusion / Exclusion criteria to whatever
           fields are present. `title` is always available; `abstract`
           and `tags` participate only when populated. Field absence
           never causes SKIP — it only narrows the screening surface.
           (Same criteria applied to corpus and external DB results.)
        d. classify entry as:
             - INCLUDE  → add to pre_screened_included[]
             - EXCLUDE  → add to pre_screened_excluded[] with reason
             - SKIP     → add to pre_screened_skipped[] with reason
                          (only when criteria cannot be applied at all
                          on the present fields — see §4 F1)
```

```
Step 2: Search-fills-gap (external DB)
    derive uncovered_topics =
      RQ subtopics − {topics covered by pre_screened_included[]}
    user_corpus_only = user explicitly asked "use my corpus only"
                       (literal phrase or unambiguous equivalent)

    case A: uncovered_topics is non-empty AND NOT user_corpus_only
        → run external DB search scoped to uncovered_topics
        → external_included[] subject to the SAME Inclusion / Exclusion criteria
    case B: uncovered_topics is empty AND user_corpus_only
        → skip external search; PRE-SCREENED block surfaces this as
          "external search omitted on user request; corpus fully covers
          RQ subtopics."
    case B': uncovered_topics is non-empty AND user_corpus_only
        → skip external search BUT surface uncovered_topics in
          PRE-SCREENED block as a known coverage gap:
          "external search omitted on user request; uncovered RQ
          subtopics: <list>. Corpus does not cover these topics;
          downstream synthesis must declare this gap."
    case C: uncovered_topics is empty AND NOT user_corpus_only
        → run STANDARD external DB search (not scope-limited; treat as if
          no corpus existed). Rationale: corpus-covered topics still need
          newer-work checks and dedup validation against external sources.
          PRE-SCREENED block surfaces this as "corpus fully covered RQ
          subtopics; external search ran independently to validate coverage
          and capture newer work."
```

```
Step 3: Merge for downstream
    final_included = pre_screened_included[] ∪ external_included[]
    final_included feeds:
        - annotated bibliography (neutral, no source tag)
        - literature matrix (academic-paper only)
        - all subsequent Phase 1 deliverables
```

```
Step 4: Emit Search Strategy Report
    - PRE-SCREENED FROM USER CORPUS: block (see §3.2 template)
    - DATABASES: block (existing structure, unchanged)
    - INCLUSION / EXCLUSION CRITERIA: block (existing, unchanged)
    - RESULTS SUMMARY: ...
```

### 3.2 `PRE-SCREENED FROM USER CORPUS:` block template

This template is pinned in `literature_corpus_consumers.md` and reproduced verbatim in both consumer agent files. The lint enforces all required line markers — structural fields plus the F3 zero-hit anchor and the F4a–f inline-comment anchors (see §5 L7).

```markdown
PRE-SCREENED FROM USER CORPUS:
- Adapter: <obtained_via enum value | "<unspecified>" | "mixed (...)">
                                          # e.g., zotero-bbt-export, or "<unspecified>" per F4a,
                                          # or "<value> (N of M entries declared)" per F4b,
                                          # or "mixed (zotero-bbt-export: K, ..., undeclared: U)" per F4c
- Snapshot date: <max(obtained_at)>        # ISO 8601, or "<unspecified>" per F4d,
                                          # or "<date> (M of N entries declared)" per F4e,
                                          # or append "(spans <N> days; corpus may not be a single snapshot)" per F4f
- Total entries scanned: <N>
- Pre-screening result:
  - Included: <K> entries
    citation_keys:
      - <k1>
      - <k2>
  - Excluded by inclusion / exclusion criteria: <E> entries
    citation_keys:
      - <e1>
    (omit this sub-block if 0)
  - Skipped (criteria cannot be applied): <S> entries
    citation_keys with reasons:
      - <key>: <reason>
    (omit this sub-block if 0)
- Zero-hit note (emit per F3 only when Included: 0):
  Zero-hit note (corpus non-empty, 0 included after screening): possible
  causes are (a) corpus is stale relative to current RQ, (b) RQ has
  shifted away from what the user originally curated, (c) adapter
  exported entries unrelated to this RQ.
- Note: presence in corpus does not imply inclusion;
  same criteria applied to corpus and external sources.
```

**PR-B implementer note.** The Adapter and Snapshot date inline comments above are the canonical reproducibility surface for F4a–F4f. The Zero-hit note is conditional (only emitted when `Included: 0`). The reference doc `academic-pipeline/references/literature_corpus_consumers.md` and `bibliography_agent.md` carry verbatim copies; PR-B must keep `literature_strategist_agent.md` aligned with this template.

**List ordering.** All `citation_keys` lists (Included, Excluded, Skipped) are sorted alphabetically by `citation_key` (lexicographic, case-sensitive). This ordering is deterministic across hand-edited or assembled passports and reproducible across runs. The sort applies before truncation.

**Truncation rule for `citation_keys` lists (Included, Excluded, Skipped).** When a list contains more than 50 entries, the inline form keeps the first 20 + last 5 entries (alphabetically) and points to an appendix file for the rest.

**Included / Excluded** (key-only):

```markdown
    citation_keys (truncated; first 20 + last 5 of <N>; full list at <appendix path>):
      - <k1>
      - <k2>
      - ...
      - <k20>
      - ... (<N - 25> entries omitted, alphabetically between k20 and k_N-4)
      - <k_N-4>
      - <k_N-3>
      - <k_N-2>
      - <k_N-1>
      - <k_N>
```

**Skipped** (key + reason; inline form preserves reasons for the first 20 + last 5):

```markdown
    citation_keys with reasons (truncated; first 20 + last 5 of <N>; full list at <appendix path>):
      - <k1>: <reason 1>
      - <k2>: <reason 2>
      - ...
      - <k20>: <reason 20>
      - ... (<N - 25> entries with reasons omitted, alphabetically between k20 and k_N-4)
      - <k_N-4>: <reason N-4>
      - <k_N-3>: <reason N-3>
      - <k_N-2>: <reason N-2>
      - <k_N-1>: <reason N-1>
      - <k_N>: <reason N>
```

The appendix file is `pre_screened_citation_keys_<list>_<timestamp>.txt`. For Included and Excluded: one citekey per line, alphabetical order. For Skipped: one `<citation_key>: <reason>` pair per line, alphabetical order by key. `<list>` is `included`, `excluded`, or `skipped`. Lists with 50 or fewer entries emit in full with no truncation marker, no appendix needed.

**Adapter values.** `<obtained_via enum value>` is one of the v3.6.4 schema enum values: `zotero-api`, `zotero-bbt-export`, `obsidian-vault`, `folder-scan`, `manual`, `other`. Do not invent variants such as `zotero_export` or `obsidian_vault`; the schema defines the canonical names.

### 3.3 Out of data-flow scope (explicit)

- Agents do not recompute aggregate metadata. They take `obtained_via` mode and `obtained_at` max directly from entries.
- Agents do not write a rejection log. `rejection_log.yaml` is a v3.6.4 adapter-side artifact; consumer agents only read passport contents.
- Agents do not redact `abstract` or `user_notes`. Privacy is the v3.6.4 adapter contract's responsibility (PRIVATE FIELD annotation).

### 3.4 Data-flow risks

1. **Inclusion / Exclusion parity.** The lint cannot enforce that an LLM applies the same criteria to corpus and external sources. Agent files use BAD / GOOD example pairs (existing ARS anti-pattern teaching style) to make the parity rule load-bearing in prose.
2. **`uncovered_topics` derivation.** Decomposing an RQ into subtopics is LLM behaviour; the spec does not prescribe an algorithm. The spec only requires the four-case invariants in §3.1 Step 2 (case A: scope external to uncovered topics; case B: corpus-only with full coverage skips external search; case B': corpus-only with partial coverage skips external search but surfaces gap; case C: standard external search runs even when corpus fully covers RQ, for newer-work validation). Detecting overlap between PRE-SCREENED `citation_keys` and DATABASES results is reserved for manual review and not enforced by lint.
3. **Reproducibility under corpus-first flow.** Reviewers reading a v3.6.5 Search Strategy Report must trust that the LLM applied the same Inclusion / Exclusion criteria to corpus and external results. Iron Rule 1 makes this a hard contract; runtime verification waits for real SR runs. The `[CORPUS PARSE FAILURE]` graceful path (§4.4 Iron Rule 4) is the only sanctioned escape route — silent corpus skip is forbidden.

(There is no SR-mode bypass. After codex round-2 review, the original §3.5 carve-out was removed because no canonical passport field maps to "the pipeline is currently running systematic_review", and inferring mode from `origin_mode` would conflate "where this passport came from" with "what mode is now consuming it". `systematic_review` mode users get reproducibility guarantees through Iron Rule 1 same-criteria parity plus Step 2 case C standard external search; PRE-SCREENED block records exactly which corpus entries entered the inclusion set, making the audit trail explicit.)

---

## 4. Failure Modes

### 4.1 Failure taxonomy

| ID | Failure | Trigger | Contract behaviour |
|---|---|---|---|
| **F1** | Inclusion / exclusion criteria cannot be applied | The criteria require a field that is absent and no surrogate field is present (e.g., criteria reference research method but `title`, `tags`, and `abstract` together convey nothing actionable) | Skip entry, record in `pre_screened_skipped[]` with reason, list in PRE-SCREENED block. Do NOT skip merely because optional fields are absent. Do NOT attempt to dereference `source_pointer` (out of v3.6.4 contract, see §4.5). |
| **F2** | Corpus unparseable OR fails minimal shape check at consumer side | (a) YAML parse fails on the passport; OR (b) parse succeeds but `literature_corpus` is not a list, OR an entry is not a mapping, OR an entry lacks any of the five v3.6.4 required fields in readable form (`citation_key` non-empty string / `title` non-empty string / `authors` non-empty list / `year` numeric-coercible / `source_pointer` non-empty string). Triggered by hand-edits, partial corruption, or assembled passports that bypass `scripts/check_literature_corpus_schema.py`. The shape check covers what Step 1 must read safely; it is NOT JSON Schema revalidation (no regex, no CSL-JSON shape, no enum, no URI format). | Emit `[CORPUS PARSE FAILURE: <one-line cause>]` in the Search Strategy Report (cause names whichever sub-case fired), then fall back to external-DB-only flow. Continue Phase 1 deliverables without corpus contribution. See §4.4 Iron Rule 4. |
| **F3** | Corpus non-empty but zero hits | RQ does not intersect with corpus (`pre_screened_included[]` is empty after Step 1) | Dispatch to Step 2 by user_corpus_only flag (NOT a fixed "always external DB" path). If NOT user_corpus_only → Step 2 case A or C runs external DB search. If user_corpus_only → Step 2 case B' runs (skip external search, surface ALL RQ subtopics as uncovered gap). PRE-SCREENED block always emits a zero-hit note listing the three plausible causes (stale corpus, RQ mismatch, adapter exporting unrelated KB) regardless of dispatch path. |
| **F4** | Provenance metadata missing or inconsistent | Some / all entries lack `obtained_via` or `obtained_at`; multiple `obtained_via` values mixed | Surface gaps and mixing in PRE-SCREENED block; never silently fill in or guess. Does not block flow. |
| **F5** | Consumer-side presence detection | Material Passport carries no `literature_corpus` field, OR `literature_corpus: []`, OR has the field as non-empty array | Consumer treats absent field and empty array identically (no corpus-first flow). Non-empty triggers corpus-first. See §4.3 for cross-contract caveat. |

### 4.2 F4 detail — provenance metadata gaps

`obtained_via` and `obtained_at` are both optional and "strongly recommended" in v3.6.4 schema. Each gets three parallel sub-cases.

**`obtained_via` (adapter origin):**

- **F4a — All entries lack `obtained_via`.** Triggered when ZERO entries declare `obtained_via`. PRE-SCREENED block writes `Adapter: <unspecified>` and a trailing note: `Adapter origin not declared; user-written adapter should populate obtained_via per v3.6.4 schema recommendation.`
- **F4b — Partial coverage, single value.** Triggered when AT LEAST ONE entry declares `obtained_via` AND all declared entries share a single value. Write `Adapter: <enum value> (N of M entries declared)`.
- **F4c — Mixed values.** Triggered when two or more distinct enum values appear among declared entries. Write `Adapter: mixed (zotero-bbt-export: K, obsidian-vault: L, ..., undeclared: U)`, using exact v3.6.4 enum values; include an `undeclared: U` count if any entries lack the field.

**`obtained_via` precedence:** F4a / F4b / F4c are mutually exclusive by construction (each requires a different declared-count condition). F4c takes precedence over F4b only conceptually — the trigger sets do not overlap once F4b's "at least one entry declares" precondition is honoured.

**`obtained_at` (snapshot timestamp):**

- **F4d — All entries lack `obtained_at`.** Write `Snapshot date: <unspecified>` and the trailing note: `Snapshot date not declared; reproducibility is reduced. Adapter should populate obtained_at per v3.6.4 schema recommendation.`
- **F4e — Partial coverage.** Write `Snapshot date: <max(obtained_at)> (M of N entries declared)`.
- **F4f — Wide spread.** When max(obtained_at) − min(obtained_at) exceeds 90 days, append `(spans <N> days; corpus may not be a single snapshot)`. Threshold is advisory; spec does not enforce.

**`obtained_at` precedence:** F4d applies only when ZERO entries declare `obtained_at`. F4e and F4f compose: when partial coverage and >90-day spread both apply, output the F4e ratio AND the F4f span suffix in the same line: `Snapshot date: <max(obtained_at)> (M of N entries declared; spans <N> days; corpus may not be a single snapshot)`.

Rationale: provenance fields are the reproducibility anchors. The optional status in v3.6.4 schema means consumers cannot demand presence, but consumers also cannot pretend partial / inconsistent provenance does not exist. F4a–f make the gaps auditable rather than silent.

### 4.3 F5 detail — consumer-side presence detection

**Important contract distinction.** The v3.6.4 input port has two contracts that must not be conflated:

- **Material Passport schema (Schema 9 in `handoff_schemas.md`).** `literature_corpus` is an optional top-level field. Absent and `[]` are both schema-valid.
- **Adapter passport contract (`academic-pipeline/references/adapters/overview.md`).** Adapters MUST emit a top-level `literature_corpus` array, even when empty. `check_literature_corpus_schema.py` rejects an adapter passport that omits the key entirely.

These contracts apply at different boundaries. v3.6.5 consumers operate against the **Material Passport schema**, not the adapter contract — by the time a passport reaches a Phase 1 agent, it may have been produced by an adapter, hand-edited, or assembled from a previous run. Some upstream sources will have `literature_corpus: []`; others will not include the field at all.

For consumer-side presence detection, both states mean the same thing: there is no corpus to consume. Neither triggers the corpus-first flow, neither triggers F3 zero-hit surfacing. This does not weaken the adapter contract; v3.6.4 lint still requires adapter output to include the key.

Rationale: a user whose adapter writes an empty array (KB temporarily has no entries matching the entry shape) should not see a misleading zero-hit warning. A user whose passport never had a literature_corpus field at all should also see the same flow, because the consumer's job is "do I have corpus to use, yes or no", not "did the upstream tool follow the adapter contract".

### 4.4 Iron Rules

Four hard rules go into `literature_corpus_consumers.md` and both consumer agent files. Pattern follows v3.4.0 compliance agent's iron-rule writing style and harness-retirement debt-pattern §2 (silent-failure domain keeps negative framing as load-bearing).

1. **Iron Rule 1 — Same criteria.** "Apply the same Inclusion / Exclusion criteria to corpus entries and external database results. No exceptions."
2. **Iron Rule 2 — No silent skip.** "Any skipped corpus entry must be recorded in the PRE-SCREENED block's skipped sub-section with a reason. Silently dropping an entry is a prompt-layer violation."
3. **Iron Rule 3 — No corpus mutation.** "Consumer agents never modify, backfill, or derive new content into `literature_corpus[]`. Read only."
4. **Iron Rule 4 — Graceful fallback on parse failure.** "Consumer agents do NOT re-validate schema, do NOT parse JSON Schema at runtime, and do NOT dereference `source_pointer` URIs. The v3.6.4 input-port lint (`scripts/check_literature_corpus_schema.py`) validates adapter output, but a passport may reach a Phase 1 agent through other paths (hand-edits, `resume_from_passport`, assembled passports). When a consumer cannot parse `literature_corpus[]`, emit `[CORPUS PARSE FAILURE: <cause>]` in the Search Strategy Report and fall back to external-DB-only flow. Do not abort Phase 1, do not attempt schema repair, do not invent contents."

Rule selection rationale:

- Iron Rule 1 enforces corpus-first parity (corpus presence ≠ automatic inclusion).
- Iron Rule 2 enforces honest reporting transparency.
- Iron Rule 3 enforces v3.6.4 input-port boundary on the write side.
- Iron Rule 4 enforces graceful failure on the read side. The input-port lint is one of several upstream paths to a passport, not all of them. When parse fails for any reason, the consumer surfaces the failure honestly and continues without corpus, mirroring v3.4.0 compliance agent's mode-aware tier-based block pattern (silent failure becomes loud failure, but Phase 1 still ships a Search Strategy Report).

### 4.5 `source_pointer` is not dereferenced (consumer boundary)

v3.6.4 adapter contract documents `source_pointer` as a stable URI back to the user's KB but explicitly does not require resolution. v3.6.5 consumers honour the same boundary: a Phase 1 agent does NOT open `zotero://...`, traverse `obsidian://...`, read `file:///...`, or HTTP-fetch any URI from a corpus entry. Source verification (resolving pointers, checking whether a paper actually exists at the URI) is `source_verification_agent`'s job, deferred to v3.6.6+.

This means F1 cannot use a 404 on `source_pointer` as a skip trigger — there is no resolution attempt to begin with. F1 fires only when the inclusion / exclusion criteria themselves cannot be applied on the present fields.

### 4.6 Failure-mode risks

1. **F1 vs F4 ambiguity.** An entry lacking `obtained_via` is an F4 case (statistical bookkeeping over the corpus); it is never an F1 case. F1 is reserved for entries where the inclusion criteria themselves cannot be evaluated on the present fields.
2. **Iron Rule 2 cross-LLM drift.** The lint enforces structural markers (PRE-SCREENED block + skipped sub-section template) but cannot verify the LLM actually records every skipped entry. Defended by BAD / GOOD example pairs in agent prose plus runtime observation in the first real SR run after ship. The `feedback_iterative_codex_audit_during_execution.md` per-gate codex audit pattern is available as an implementation-time defence (not part of the spec).
3. **Iron Rule 4 graceful fallback drift.** The lint enforces the rule's prose presence in agent.md (L5), but cannot verify the LLM actually emits `[CORPUS PARSE FAILURE: <cause>]` and falls back cleanly under live parse-failure conditions. Defended by BAD / GOOD example pair in `literature_corpus_consumers.md` showing one valid fallback report and one anti-pattern (silent skip without surface). Real SR runs validate behaviour.

---

## 5. Testing & Lint

### 5.1 Testing philosophy

H1 lint-only is the chosen testing strategy. The spec deliberately excludes deterministic behaviour tests, prose-match fixtures, and cross-model probes from the v3.6.5 ship gate.

- The lint enforces **prompt-layer structural invariants** — agent files contain the right guidance, the right structural markers, and the right backpointers.
- LLM behaviour validation is reserved for real systematic-review runs after ship and for ad-hoc per-artifact codex review during implementation. Neither is a CI gate.
- This treatment matches v3.5.1 reading-check probe (also lint-only) and avoids the prose-fixture brittleness that made earlier prompt-layer tests collapse on minor wording changes.

### 5.2 Nine lint invariants

`scripts/check_corpus_consumer_protocol.py` enforces:

| ID | Invariant | Check method |
|---|---|---|
| **L1** | `academic-pipeline/references/literature_corpus_consumers.md` exists | file exists |
| **L2** | Reference doc contains a `## Consumer:` heading for every manifest entry, plus optional stub blocks. Each non-stub block must NOT carry the stub marker; each stub block MUST carry the stub marker AND the `<!-- LINT_STUB: skip_cross_check -->` HTML comment that exempts it from §5.2.1 cross-check below. | markdown headings `## Consumer: <agent_basename>` exist for every manifest entry; any additional `## Consumer:` heading either is a manifest entry OR contains both `**Status:** Stub — implementation in PR-B (v3.6.5)` and `<!-- LINT_STUB: skip_cross_check -->` |
| **L3** | Each agent.md listed in the manifest carries a backpointer to the consumer protocol doc | grep `academic-pipeline/references/literature_corpus_consumers.md` against each manifest entry's `agent_path` |
| **L4** | Each manifested agent.md contains the PRE-SCREENED block markdown template | grep template start `PRE-SCREENED FROM USER CORPUS:` inside a fenced markdown block |
| **L5** | Each manifested agent.md contains all four Iron Rule headings | grep four iron-rule titles (Same criteria / No silent skip / No corpus mutation / Graceful fallback on parse failure) |
| **L6** | Each manifested agent.md contains the corpus-first flow Step 0 – Step 4 description AND the four-case Step 2 split | grep five step headings (presence detection / pre-screen / search-fills-gap / merge / emit report) plus the four Step 2 case markers (case A / case B / case B' / case C) |
| **L7** | PRE-SCREENED block template contains all required line markers and the truncation rule prose | grep the line markers inside the fenced block: structural lines (Adapter / Snapshot date / Total entries scanned / Pre-screening result / Included / Excluded / Skipped / citation_keys / Note about same criteria) PLUS the F3 zero-hit anchor `Zero-hit note (emit per F3 only when Included: 0)` PLUS the F4 inline-comment anchors `per F4a` / `per F4b` / `per F4c` / `per F4d` / `per F4e` / `per F4f`. Outside the fenced block: a "Truncation rule" or "truncation rule" mention in surrounding prose. The marker scoping is per-fenced-block — full-file substring matching is forbidden, because a marker name appearing in surrounding prose must not mask its deletion from the template body. |
| **L8** | `handoff_schemas.md` Schema 9 caveat state matches manifest tuple set | The manifest's `supported_consumers[]` MUST exactly match one of two closed sets of `(agent_basename, agent_path)` tuples, or L8 fails: <br>(i) **PR-A pre-release set:** `{(bibliography_agent, deep-research/agents/bibliography_agent.md)}` (single tuple). Lint REQUIRES grep-positive for `Consumer-side integration deferred to v3.6.5+` (caveat MUST remain). Caveat retirement is forbidden in PR-A because the second consumer has not shipped. <br>(ii) **PR-B release set:** PR-A tuple PLUS `(literature_strategist_agent, academic-paper/agents/literature_strategist_agent.md)`. Lint REQUIRES grep-negative for `Consumer-side integration deferred to v3.6.5+` AND grep-positive for backpointer to `literature_corpus_consumers.md`. <br>Any other manifest state fails L8: empty, typoed agent name, strategist-only, additional unknown consumer, duplicate `(basename, path)` tuple, or duplicate basename with differing path (the closed set is on tuples, not basenames, to defend against frozenset collapse). |
| **L9** | Reference doc contains a BAD / GOOD example pair illustrating Iron Rule 2 | grep `<!-- BAD -->` and `<!-- GOOD -->` HTML-comment markers adjacent to fenced example blocks (full-file substring presence; the markers are siblings of the fences, not children of them) |

#### 5.2.1 Manifest ↔ reference-doc cross-check (replaces former bidirectional invariant)

Every entry in `scripts/corpus_consumer_manifest.json` must correspond to a non-stub `## Consumer: <agent_basename>` heading in `literature_corpus_consumers.md`. Every non-stub `## Consumer:` heading must correspond to a manifest entry. Stub blocks are exempt — they exist precisely to declare "this consumer is planned but not yet shipped"; the `<!-- LINT_STUB: skip_cross_check -->` HTML comment is the explicit opt-out.

The cross-check is an L2 sub-invariant, not a separate L. The lint reports any mismatch as `L2: manifest/reference-doc divergence — <details>`.

### 5.3 Lint implementation pattern

Pattern follows v3.6.4 commit `2b81117` (`check_literature_corpus_schema.py`) and v3.4.0 commit `42575ff` (`check_prisma_trAIce_freshness.py`).

- Pure Python, no external dependencies for grep-style checks (jsonschema only for the manifest validation).
- Exit 0 = pass, exit 1 = fail.
- One function per invariant; the main loop runs all checks and aggregates a fail list before printing.
- TDD red-green: each invariant has at least one positive test and one negative test (mirrors v3.4.0 commit `ee91335`'s 13 failing-tests-first pattern).
- Estimated unit tests: 9 invariants × (1 positive + 1 negative) = 18 tests, plus one integration test that runs the full lint against the actual v3.6.5 files. Approximately 20 tests total.

### 5.4 CI wiring

`feedback_codex_review_per_schema_increment.md` recorded the v3.4.0 PRISMA-trAIce freshness anti-pattern of writing a lint but forgetting to wire CI. v3.6.5 must wire:

```yaml
- name: Check corpus consumer protocol
  run: python scripts/check_corpus_consumer_protocol.py
```

Placement: after the `check_literature_corpus_schema` step in `.github/workflows/spec-consistency.yml` (same artifact family, ordered input → consumer).

### 5.5 PR-A vs PR-B lint partition (manifest-driven)

The challenge: PR-A merges before `literature_strategist_agent` is implemented, but the reference doc may already preview the second consumer block (so PR-B's append is clean). The lint must pass during PR-A without falsely claiming `literature_strategist_agent` is supported.

Solution: a manifest file `scripts/corpus_consumer_manifest.json` lists currently-supported consumers. L3, L4, L5, L6 iterate manifest entries only. L2 (with sub-invariant 5.2.1) treats stub blocks as explicitly opted out via the `<!-- LINT_STUB: skip_cross_check -->` marker, so PR-A's stub block does not falsely trigger cross-check failure.

```json
{
  "supported_consumers": [
    {
      "agent_path": "deep-research/agents/bibliography_agent.md",
      "skill": "deep-research",
      "since_version": "v3.6.5",
      "phase": "Phase 1"
    }
  ]
}
```

PR-B appends the second entry. Two alternative approaches were rejected:

- **Prose-marker-only opt-in (no manifest):** rejected — semantic prose markers as lint inputs are fragile (per `feedback_codex_review_per_schema_increment` cascade history).
- **Disable L3 in PR-A, enable in PR-B:** rejected — violates the H1 "ship-equals-enforce" premise; lint should never be half-enabled at ship time.

### 5.6 Out of lint scope (explicit)

- Whether the LLM actually executes the corpus-first flow at runtime.
- Whether the PRE-SCREENED block appears in real generated reports.
- Whether `citation_keys` listed correspond to RQ-relevant content.
- Whether external DB results actually avoid overlap with corpus inclusions.

All four are LLM behaviour-layer concerns. Validation is deferred to runtime observation in real SR runs.

### 5.7 Testing risks

1. **Manifest sync drift handling.** The manifest ↔ reference-doc cross-check (§5.2.1) is the lint's primary structural defence. Stub blocks are explicitly opted out via the `<!-- LINT_STUB: skip_cross_check -->` marker; this means the PR-A → PR-B promotion (stub → non-stub) is a single deletion of the marker, not a rewrite of multiple invariants.
2. **L9 BAD / GOOD example fragility.** Codex review historically catches inconsistencies in author-written BAD examples (R3 deep-semantic-hole pattern, v3.6.3). Implementation should expect a codex round-2 review to catch L9 example issues; plan a buffer round.
3. **PR-B lint changes.** Adding the second manifest entry, removing the stub marker, and any per-agent specialisations in L4 / L7 / L9 means PR-B touches the lint and tests. The PR-B implementation plan must list "lint changes" as an explicit task.

---

## 6. Rollout & Version Sweep

### 6.0 Splitting principle (PR-A single-consumer pre-release / PR-B release sweep)

**Naming.** PR-A is the **single-consumer pre-release**: it ships one live consumer (`bibliography_agent`) plus the reference-doc skeleton, manifest, lint, and CI wiring, but adds no public release artifacts (no version bump, no CHANGELOG, no caveat retirement). PR-B is the **release sweep**: it ships the second consumer (`literature_strategist_agent`), promotes the stub block, applies the version sweep, retires the v3.6.4 deferred caveat, writes the CHANGELOG entry, and tags v3.6.5.

The v3.6.5 release narrative is "consumer integration shipped". Until BOTH consumer agents are implemented, no main-branch artifact may claim v3.6.5 has shipped. Codex review of the original split caught a false-release-state risk: if PR-A bumps suite version, retires the `Consumer-side integration deferred to v3.6.5+` caveat, and writes a `[3.6.5]` CHANGELOG entry while one of two named consumers is untouched, readers of `main` will see contradictory signals (CHANGELOG says shipped, agent file shows nothing).

The split is therefore:

- **PR-A is the single-consumer pre-release.** It ships one live consumer (`bibliography_agent` reads `literature_corpus[]`) plus the consumer-side reading rules, the reference doc skeleton (with a stub block for the second consumer), the manifest, the lint, the CI wiring, and the lint tests. PR-A does NOT bump suite version. PR-A does NOT retire the v3.6.4 deferred caveat. PR-A does NOT add a `[3.6.5]` CHANGELOG entry. After PR-A merges, `main` carries one functional consumer and one stub; nothing publicly claims v3.6.5 has shipped.
- **PR-B is the release sweep.** It implements `literature_strategist_agent`, removes the stub marker, appends to the manifest, applies the seven-touchpoint version sweep, retires the deferred caveat in `handoff_schemas.md`, writes the `[3.6.5]` CHANGELOG entry, and tags v3.6.5 on the merge commit.

This shape solves three problems the original codex review surfaced: no false release state during the stub window, no two-PR CHANGELOG rebase conflict, and no unusual "tag on PR-B but version bump on PR-A" sequence for release ops to remember.

### 6.1 PR-A — `feat/v3.6.5-corpus-consumer-deep-research` (single-consumer pre-release)

- **Base:** `main` at v3.6.4 (`aa96fa6`).
- **Scope:**
  - Add `academic-pipeline/references/literature_corpus_consumers.md`. Contains a `## Consumer: bibliography_agent` block with full content, AND a `## Consumer: literature_strategist_agent` stub block carrying both `**Status:** Stub — implementation in PR-B (v3.6.5)` and the `<!-- LINT_STUB: skip_cross_check -->` HTML comment.
  - Add `scripts/corpus_consumer_manifest.json` containing only the `bibliography_agent` entry.
  - Add `scripts/check_corpus_consumer_protocol.py` and tests. All nine invariants enabled (with §5.2.1 sub-invariant). L3 / L4 / L5 / L6 iterate manifest entries; L2 honours the stub marker.
  - Modify `deep-research/agents/bibliography_agent.md`: add Step 0 presence detection, corpus-first flow, four Iron Rules, PRE-SCREENED template, backpointer.
  - Modify `.github/workflows/spec-consistency.yml`: add lint step.
- **Explicitly untouched:**
  - `academic-paper/agents/literature_strategist_agent.md` (PR-B scope).
  - All SKILL.md version fields, `MODE_REGISTRY.md`, `.claude/CLAUDE.md` Skills Overview, `README.md`, `README.zh-TW.md`, `CHANGELOG.md` (PR-B scope).
  - `shared/handoff_schemas.md` Schema 9 deferred caveat (PR-B scope).
  - `scripts/check_spec_consistency.py` (PR-B scope).
- **Tag / release:** none. PR-A merge does not produce a release artifact.

### 6.2 PR-B — `feat/v3.6.5-release-sweep` (release sweep)

- **Base:** `main` after PR-A merge.
- **Scope:**
  - Modify `academic-paper/agents/literature_strategist_agent.md`: add Step 0 presence detection, corpus-first flow, four Iron Rules, PRE-SCREENED template, backpointer. Literature-matrix and gap-identification sections gain corpus-aware additions.
  - Update `academic-pipeline/references/literature_corpus_consumers.md`: promote stub block to full content; remove `<!-- LINT_STUB: skip_cross_check -->` marker AND `**Status:** Stub` line.
  - Update `scripts/corpus_consumer_manifest.json`: append `literature_strategist_agent` entry.
  - Update `scripts/check_corpus_consumer_protocol.py` if any per-agent specialisations are needed in L4 / L7 / L9. Update tests.
  - Update `shared/handoff_schemas.md` Schema 9: retire the `Consumer-side integration deferred to v3.6.5+` caveat; replace with backpointer to `literature_corpus_consumers.md`.
  - Apply the seven-touchpoint version sweep (see §6.3).
  - Add `CHANGELOG.md` `[3.6.5]` entry.
  - Tag `v3.6.5` annotated on PR-B merge commit; create GitHub release.

### 6.3 Seven version-sweep touchpoints (PR-B only)

Per `feedback_version_bump_sweep_checklist.md`:

1. `academic-pipeline/SKILL.md` frontmatter `version` and footer Skill Info: `3.6.4 → 3.6.5`.
2. `deep-research/SKILL.md` frontmatter `version` and footer Skill Info: `2.9.x → 2.9.(x+1)` patch bump.
3. `academic-paper/SKILL.md` frontmatter `version` and footer Skill Info: `3.1.x → 3.1.(x+1)` patch bump.
4. `MODE_REGISTRY.md` last-updated → `v3.6.5`.
5. `.claude/CLAUDE.md` Skills Overview table — three skill versions aligned.
6. `README.md` and `README.zh-TW.md` — badge / heading / changelog entry (Chinese version uses full-width parentheses).
7. `scripts/check_spec_consistency.py` version pin sweep — keep v3.3.2 – v3.6.4 lines as regression guards; append v3.6.5 expected heading and lint pin.

**Critical:** v3.6.4 commit `d54bd78` recorded the lesson that `check_spec_consistency.py` cross-validates `CHANGELOG.md`. The seven-touchpoint sweep must occur in a single atomic commit covering SKILL.md, CHANGELOG, and spec consistency lint together. Splitting the sweep causes intermediate CI red.

### 6.4 Tag and release narrative

- PR-A merge: no tag, no release.
- PR-B merge: tag `v3.6.5` annotated on the PR-B merge commit. GitHub release titled `v3.6.5 — Material Passport literature_corpus[] Consumer Integration`, with both PR links in release notes.
- Rationale: PR-A is infrastructure with no user-facing claim. PR-B is the release. Tag and version bump live together in PR-B; release ops never need a "remember to tag elsewhere" footnote.

### 6.5 `CHANGELOG.md` `[3.6.5]` structure (added by PR-B)

```markdown
## [3.6.5] - 2026-04-DD

### Added
- Material Passport `literature_corpus[]` consumer integration in Phase 1
  (deep-research/bibliography_agent + academic-paper/literature_strategist_agent).
  Corpus-first, search-fills-gap flow with PRE-SCREENED reproducibility block.
  Reproducibility for systematic-review use is preserved through Iron Rule 1
  same-criteria parity plus Step 2 case C (standard external search runs even
  when corpus fully covers RQ subtopics).
- `academic-pipeline/references/literature_corpus_consumers.md` — consumer protocol
  reference with four Iron Rules (Same criteria / No silent skip / No corpus mutation /
  Graceful fallback on parse failure) and per-consumer reading instructions.
- `scripts/check_corpus_consumer_protocol.py` — CI lint enforcing nine protocol invariants
  with manifest-driven consumer list and stub-block opt-out.
- `scripts/corpus_consumer_manifest.json` — supported-consumer manifest.

### Changed
- `shared/handoff_schemas.md` Schema 9 — retired the v3.6.4 "Consumer-side integration
  deferred to v3.6.5+" caveat; replaced with backpointer to the consumer protocol.
- `deep-research/SKILL.md` 2.9.x → 2.9.(x+1) — bibliography_agent corpus-first flow.
- `academic-paper/SKILL.md` 3.1.x → 3.1.(x+1) — literature_strategist_agent corpus-first flow.
- `academic-pipeline/SKILL.md` 3.6.4 → 3.6.5 — suite version invariant.

### Notes
- Consumer integration is presence-based: auto-engages when passport carries a
  non-empty `literature_corpus[]` and parses cleanly. Parse failures fall back
  to external-DB-only flow with a `[CORPUS PARSE FAILURE]` surface. No new env
  flag introduced.
- Schema is unchanged from v3.6.4. Existing user adapters work without modification.
- `citation_compliance_agent` corpus integration deferred to v3.6.6+.
- `source_pointer` is not dereferenced by consumers; URI resolution remains a future
  `source_verification_agent` concern.
```

### 6.6 Pre-merge gate sequence (per PR)

1. Local: `python scripts/check_corpus_consumer_protocol.py` exit 0.
2. Local: `python scripts/check_spec_consistency.py` exit 0.
3. Local: `pytest scripts/` (adapter tests + new lint tests + existing suite) all green.
4. Push branch — CI `spec-consistency.yml` and `pytest.yml` all green.
5. Per-artifact codex review during construction: consumer protocol doc, lint script, agent.md change, version sweep (PR-B only) — each receives an incremental review. This avoids the v3.6.4 PR #40 4566-line codex hang lesson by keeping each review's input bounded. Pattern from `feedback_codex_review_per_schema_increment.md`.
6. `/simplify` code review pass.
7. PR open with `--base main`.
8. Optional final codex review on full PR diff if the diff remains digestible. If the PR exceeds the v3.6.4 hang threshold, rely on per-artifact review history.

### 6.7 Rollout risks

1. **Stub-period exposure.** Between PR-A merge and PR-B merge, `main` carries the stub block in the reference doc and the manifest with one entry. Because PR-A does NOT touch CHANGELOG, version, README, or `handoff_schemas.md` Schema 9 caveat, no public artifact claims v3.6.5 has shipped. The stub block itself is the only visible signal, and its `Status: Stub — implementation in PR-B (v3.6.5)` text is honest about state.
2. **Stub window length.** Spec mandates a maximum stub window of five working days. If PR-B cannot ship within that window, the safe rollback is described in §6.8.
3. **PR-B is bigger than PR-A.** PR-B touches the literature_strategist agent, the manifest, the reference doc (stub → full), seven version-sweep touchpoints, CHANGELOG, and `handoff_schemas.md`. The implementation plan must split PR-B into bounded subtasks for incremental codex review (consumer prose / version sweep / changelog / schema caveat retirement). Single-commit atomicity applies only to the version-sweep cluster (per §6.3 critical note).
4. **Tag is on PR-B merge commit.** This is now the natural location (PR-B does the version bump); no unusual instruction needed for release ops.

### 6.8 PR-B blocked rollback

If PR-B cannot ship within five working days of PR-A merge:

- Open `revert/v3.6.5-corpus-consumer-deep-research` PR that reverts PR-A's changes from `main`. PR-A's lint, manifest, reference doc, and bibliography_agent edits are removed.
- Reason: keeping single-consumer pre-release on `main` indefinitely creates a half-state where the bibliography_agent reads corpus but no other documentation acknowledges v3.6.5 exists. Five days is the upper bound for that ambiguity.
- The revert is a clean rollback because PR-A made no version / release / public-doc claims.
- After revert, re-plan v3.6.5 as a single PR (path 1) or restart the two-PR sequence with new dates.

---

## 7. Post-Ship Follow-ups (Backlog Anchors)

These items are recorded for the project memory but are out of v3.6.5 scope:

- Real SR run observation: corpus-first flow drift, silent-skip rate, criteria-parity adherence under live LLM behaviour.
- v3.6.6+ candidates: `citation_compliance_agent` Phase 5a corpus reading; `source_verification_agent` corpus-based source cross-checking.
- Real-user adapter samples feeding back into v3.6.4 reference adapter expansions.

---

## 8. Decision Log Summary

| Q | Topic | Decision |
|---|---|---|
| Q1 | Consumer scope | A — `bibliography_agent` + `literature_strategist_agent`; `citation_compliance_agent` not in v3.6.5 |
| Q2 | Phase 1 flow | A1 — Corpus-first, search-fills-gap |
| Q3 | Opt-in mechanism | B1 — Presence-based auto-read |
| Q4 | Failure mode philosophy | C-honest — Skip individual entries but surface to user |
| Q5 | PRISMA-style record | D2 — Independent `PRE-SCREENED FROM USER CORPUS:` block |
| Q6 | Annotated bib attribution | E2 — Bibliography stays neutral; attribution lives in search documentation |
| Q7 | Schema impact | F1 — No schema change; retire deferred caveats only |
| Q8 | Reference doc location | G1 — `academic-pipeline/references/literature_corpus_consumers.md` |
| Q9 | Testing strategy | H1 — Lint-only, nine invariants, CI wired |
| Q10 | Version bump | I1 — Suite v3.6.5 patch |
| Q11 | Implementation order | Path 3 — Two PRs, one v3.6.5 tag at PR-B |
| Q12 | Which PR first | PR-A deep-research first; PR-B academic-paper second |

### Codex review round 1 fix audit (2026-04-26)

Round-1 codex review (gpt-5.5 high) on commit `3631803` returned 6 P1 + 5 P2. All 11 findings audited:

| Round-1 ID | Topic | Decision / fix location |
|---|---|---|
| R1-P1-1 | F5 cross-contract clarity | Distinguish Material Passport schema (`literature_corpus` optional) from adapter passport contract (top-level `literature_corpus` required); v3.6.5 consumers operate against Material Passport schema (§4.3) |
| R1-P1-2 | F1 trigger | Remove `source_pointer` 404 trigger; F1 fires only when criteria cannot be applied to present fields (§4.1, §4.5) |
| R1-P1-3 | Step 1.c screening | Apply criteria to whatever fields are present; field absence narrows the screening surface but never causes SKIP (§3.1) |
| R1-P1-4 | PR-A false release state | PR-A becomes single-consumer pre-release; version sweep, CHANGELOG, deferred-caveat retirement, and tag all move to PR-B (§6.0, §6.1, §6.2) |
| R1-P1-5 | Manifest cross-check inconsistency | §5.2 L2 + §5.2.1 sub-invariant + `<!-- LINT_STUB: skip_cross_check -->` marker for stub blocks (§5.2) |
| R1-P1-6 | Iron Rule 4 feasibility (round-1) | Rewrite as "trust the input port" contract; keep four rules (§4.4) — *superseded by R2-P1-2 below* |
| R1-P2-1 | F4 obtained_at coverage | Add F4d/e/f sub-cases for obtained_at missing/partial/wide-spread, parallel to F4a/b/c (§4.2) |
| R1-P2-2 | Provenance enum drift (upgraded to P1-tier in fix) | Use exact v3.6.4 schema enum values (`zotero-bbt-export`, `obsidian-vault`, `folder-scan`); spec demonstrates correct values and warns against drift (§3.2, §4.2) |
| R1-P2-3 | systematic_review bias (round-1) | SR mode bypasses corpus-first entirely (§3.5 added) — *superseded by R2-P1-3 below* |
| R1-P2-4 | Citation_keys unbounded list | Truncation rule for >50 entries; alphabetical ordering; appendix file for full list (§3.2) |
| R1-P2-5 | Two-PR overcomplication | Resolved by R1-P1-4 PR-A scope split |

### Codex review round 2 fix audit (2026-04-26)

Round-2 fresh codex review on commit `f75319c` returned 4 P1 + 4 P2. All 8 findings audited:

| Round-2 ID | Topic | Decision / fix location |
|---|---|---|
| R2-P1-1 | L8 vs PR-A scope contradiction | L8 is now manifest-gated: caveat retirement is required only when manifest contains both consumers (§5.2 L8) |
| R2-P1-2 | Iron Rule 4 still conflated adapter contract with consumer boundary | Rewrite as "Graceful fallback on parse failure": `[CORPUS PARSE FAILURE: <cause>]` and external-DB-only fallback; F2 changed from Reserved to corpus-unparseable case (§4.1 F2, §4.4 Iron Rule 4) — *supersedes R1-P1-6* |
| R2-P1-3 | SR-mode bypass not executable | Removed entirely. No canonical passport field maps to "currently running systematic_review"; reproducibility relies on Iron Rule 1 same-criteria parity plus §3.1 Step 2 case C standard external search (§3.1 Step 0, §3.4 risk #3) — *supersedes R1-P2-3* |
| R2-P1-4 | Step 2 empty-set bug | Step 2 split into three explicit cases (A: scope to uncovered; B: corpus-only on user request; C: empty uncovered + no user request → standard external search anyway) (§3.1 Step 2) |
| R2-P2-1 | F4 sub-case overlap | Precedence rules: F4c > F4b for `obtained_via`; F4e and F4f compose for `obtained_at` (§4.2) |
| R2-P2-2 | Truncation rule LLM-ambiguous | Alphabetical ordering pinned; multi-line list format unified across all three sub-blocks (§3.2) — *Skipped-never-truncated stance superseded by R3-P2-2* |
| R2-P2-3 | "Infra-only PR-A" inaccurate | §6.0 adds naming caveat (§6.0) — *partially fixed; full propagation in R3-P2-3* |
| R2-P2-4 | Decision log audit gap | This table now lists every R1 and R2 finding with traceability (§8) |

### Codex review round 3 fix audit (2026-04-26)

Round-3 fresh codex review on commit `04e2cbf` returned 4 P1 + 3 P2. All 7 findings audited:

| Round-3 ID | Topic | Decision / fix location |
|---|---|---|
| R3-P1-1 | Step 2 mishandles corpus-only when uncovered_topics is non-empty | Add Case B': corpus-only request with uncovered topics → skip external search BUT surface uncovered_topics as known coverage gap in PRE-SCREENED block. Case A precondition adjusted to "uncovered non-empty AND NOT user_corpus_only" (§3.1 Step 2) |
| R3-P1-2 | Iron Rule 4 rename did not propagate | Sweep "Trust the input port" → "Graceful fallback on parse failure" across L5 grep target and CHANGELOG draft. Audit ledger entries from R1 / R2 retain their original wording as historical record of what each round actually changed. |
| R3-P1-3 | F2 fallback YAML-valid / schema-invalid hole | Step 0 splits into three branches (no corpus / parse-or-shape fail / corpus-first). F2 trigger expanded to cover (a) YAML parse fail OR (b) parse OK but minimal shape check fails (literature_corpus is a list AND each entry is a mapping AND each entry has citation_key string). Shape check is LLM-inspectable and does NOT violate Iron Rule 4's "no JSON Schema revalidation" (§3.1 Step 0, §4.1 F2) |
| R3-P1-4 | L8 manifest gating accepts invalid third states | Closed-set match: manifest must equal `{bibliography_agent}` (PR-A) OR `{bibliography_agent, literature_strategist_agent}` (PR-B). Empty / typoed / strategist-only / extra-unknown states all fail L8 (§5.2 L8) |
| R3-P2-1 | F4b vacuous truth on zero declarations | F4b precondition tightened: "AT LEAST ONE entry declares obtained_via AND all declared share single value". F4a / F4b / F4c are now mutually exclusive by construction (§4.2) |
| R3-P2-2 | Skipped-never-truncated breaks report size | Skipped list now follows the same truncation rule as Included / Excluded (>50 entries → first 20 + last 5 + appendix). Skipped appendix preserves `<key>: <reason>` shape, so no information loss (§3.2) — *supersedes R2-P2-2 Skipped-never-truncated stance* |
| R3-P2-3 | Infra-only naming half-fixed | Propagated "single-consumer pre-release" to §1.3 Scope summary, §6.1 heading, §6.1 PR-A bullet, and §6.8 rollback rationale. "Infra-only" remains in §8 audit references for traceability (§1.3, §6.0, §6.1, §6.8) |

### Codex review round 4 fix audit (2026-04-26)

Round-4 fresh codex review on commit `4611f2b` returned 3 P1 + 2 P2. All 5 findings audited:

| Round-4 ID | Topic | Decision / fix location |
|---|---|---|
| R4-P1-1 | F2 minimal shape vs Step 1 read assumptions | Step 0 minimal shape check expanded from `citation_key` only to all five v3.6.4 required fields in readable form: citation_key (string), title (string), authors (list), year (numeric-coercible), source_pointer (string). Still NOT JSON Schema validation: no regex, no CSL-JSON shape, no enum, no URI format. Iron Rule 4 boundary preserved. (§3.1 Step 0, §4.1 F2) |
| R4-P1-2 | F3 vs Step 2 B' contradiction | F3 contract changed from "always continue to external DB" to "dispatch to Step 2 by user_corpus_only flag". Zero-hit + user_corpus_only routes through B'; zero-hit + NOT user_corpus_only routes through case A or C. PRE-SCREENED block always emits zero-hit note regardless of dispatch. (§4.1 F3) |
| R4-P1-3 | L8 PR-A false-release state | L8 PR-A branch tightened from "EITHER caveat state acceptable" to "caveat MUST remain". Caveat retirement is forbidden in PR-A; PR-B is the only state where retirement is allowed. (§5.2 L8) |
| R4-P2-1 | Skipped truncation template ambiguous | §3.2 truncation rule split into two explicit sub-templates: Included/Excluded (key-only) and Skipped (key+reason). Skipped inline form preserves `<key>: <reason>` for both first 20 and last 5 entries. (§3.2) |
| R4-P2-2 | Decision log overclaims R3 sweep | R3-P1-2 row corrected: sweep covered L5 grep target and CHANGELOG draft only. R1 / R2 audit ledger entries keep their original wording as historical record of what each round actually changed. (§8) |

---
