literature_corpus[] Consumer IntegrationStatus: 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
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.
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+.academic-paper-reviewer suite.bibliography_agent + literature_strategist_agent)academic-pipeline/references/literature_corpus_consumers.md)scripts/check_corpus_consumer_protocol.py) + unit testsscripts/corpus_consumer_manifest.json)shared/handoff_schemas.md Schema 9 paragraph: retire Consumer-side integration deferred to v3.6.5+ caveat (PR-B only).github/workflows/spec-consistency.yml[ 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)
obtained_via, obtained_at, adapter_name, adapter_version, citation_key).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.PRE-SCREENED FROM USER CORPUS: block. The annotated bibliography stays neutral with no source-attribution tags.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.
citation_compliance_agent Phase 5a corpus integration → v3.6.6+source_verification_agent corpus-based cross-checking → v3.6.6+literature_corpus_consumers.md second consumer block is a stub during the PR-A → PR-B window (see §6 Rollout).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.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: ...
PRE-SCREENED FROM USER CORPUS: block templateThis 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).
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):
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):
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.
obtained_via mode and obtained_at max directly from entries.rejection_log.yaml is a v3.6.4 adapter-side artifact; consumer agents only read passport contents.abstract or user_notes. Privacy is the v3.6.4 adapter contract's responsibility (PRIVATE FIELD annotation).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.[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.)
| 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. |
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):
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.obtained_via AND all declared entries share a single value. Write Adapter: <enum value> (N of M entries declared).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):
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.Snapshot date: <max(obtained_at)> (M of N entries declared).(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.
Important contract distinction. The v3.6.4 input port has two contracts that must not be conflated:
handoff_schemas.md). literature_corpus is an optional top-level field. Absent and [] are both schema-valid.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".
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).
literature_corpus[]. Read only."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:
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.
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.feedback_iterative_codex_audit_during_execution.md per-gate codex audit pattern is available as an implementation-time defence (not part of the spec).[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.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.
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: (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. (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. 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) |
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>.
Pattern follows v3.6.4 commit 2b81117 (check_literature_corpus_schema.py) and v3.4.0 commit 42575ff (check_prisma_trAIce_freshness.py).
ee91335's 13 failing-tests-first pattern).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:
- 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).
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.
{
"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:
feedback_codex_review_per_schema_increment cascade history).citation_keys listed correspond to RQ-relevant content.All four are LLM behaviour-layer concerns. Validation is deferred to runtime observation in real SR runs.
<!-- 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.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:
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.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.
feat/v3.6.5-corpus-consumer-deep-research (single-consumer pre-release)main at v3.6.4 (aa96fa6).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.scripts/corpus_consumer_manifest.json containing only the bibliography_agent entry.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.deep-research/agents/bibliography_agent.md: add Step 0 presence detection, corpus-first flow, four Iron Rules, PRE-SCREENED template, backpointer..github/workflows/spec-consistency.yml: add lint step.academic-paper/agents/literature_strategist_agent.md (PR-B scope).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).feat/v3.6.5-release-sweep (release sweep)main after PR-A merge.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.academic-pipeline/references/literature_corpus_consumers.md: promote stub block to full content; remove <!-- LINT_STUB: skip_cross_check --> marker AND **Status:** Stub line.scripts/corpus_consumer_manifest.json: append literature_strategist_agent entry.scripts/check_corpus_consumer_protocol.py if any per-agent specialisations are needed in L4 / L7 / L9. Update tests.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.CHANGELOG.md [3.6.5] entry.v3.6.5 annotated on PR-B merge commit; create GitHub release.Per feedback_version_bump_sweep_checklist.md:
academic-pipeline/SKILL.md frontmatter version and footer Skill Info: 3.6.4 → 3.6.5.deep-research/SKILL.md frontmatter version and footer Skill Info: 2.9.x → 2.9.(x+1) patch bump.academic-paper/SKILL.md frontmatter version and footer Skill Info: 3.1.x → 3.1.(x+1) patch bump.MODE_REGISTRY.md last-updated → v3.6.5..claude/CLAUDE.md Skills Overview table — three skill versions aligned.README.md and README.zh-TW.md — badge / heading / changelog entry (Chinese version uses full-width parentheses).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.
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.CHANGELOG.md [3.6.5] structure (added by PR-B)## [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.
python scripts/check_corpus_consumer_protocol.py exit 0.python scripts/check_spec_consistency.py exit 0.pytest scripts/ (adapter tests + new lint tests + existing suite) all green.spec-consistency.yml and pytest.yml all green.feedback_codex_review_per_schema_increment.md./simplify code review pass.--base main.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.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).If PR-B cannot ship within five working days of PR-A merge:
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.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.These items are recorded for the project memory but are out of v3.6.5 scope:
citation_compliance_agent Phase 5a corpus reading; source_verification_agent corpus-based source cross-checking.| 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 |
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 |
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) |
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) |
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) |