aboutsummaryrefslogtreecommitdiffstats
path: root/.agents/skills/zotero-obsidian-bridge
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /.agents/skills/zotero-obsidian-bridge
downloadwww-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.tar.gz
www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to '.agents/skills/zotero-obsidian-bridge')
-rw-r--r--.agents/skills/zotero-obsidian-bridge/SKILL.md112
-rw-r--r--.agents/skills/zotero-obsidian-bridge/examples/example-collection-inventory.md27
-rw-r--r--.agents/skills/zotero-obsidian-bridge/references/COLLECTION-INVENTORY-SCHEMA.md49
-rw-r--r--.agents/skills/zotero-obsidian-bridge/references/LOCAL-ZOTERO-FALLBACK.md22
-rw-r--r--.agents/skills/zotero-obsidian-bridge/references/PAPER-NOTE-SCHEMA.md81
-rw-r--r--.agents/skills/zotero-obsidian-bridge/references/WORKFLOW.md83
-rwxr-xr-x.agents/skills/zotero-obsidian-bridge/scripts/verify_paper_notes.py212
7 files changed, 586 insertions, 0 deletions
diff --git a/.agents/skills/zotero-obsidian-bridge/SKILL.md b/.agents/skills/zotero-obsidian-bridge/SKILL.md
new file mode 100644
index 0000000..577e209
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/SKILL.md
@@ -0,0 +1,112 @@
+---
+name: zotero-obsidian-bridge
+description: Use this skill when papers are collected in Zotero but the user wants detailed reading notes, project-linked literature synthesis, collection-wide paper-note coverage checks, and a connected knowledge map inside the bound Obsidian project knowledge base.
+version: 0.2.0
+---
+
+# Zotero Obsidian Bridge
+
+Bridge **Zotero as the literature source of truth** with **Obsidian as the durable project knowledge base**.
+
+Use this skill when the user wants to:
+- collect and organize papers in Zotero,
+- read papers from Zotero MCP,
+- create or update one detailed paper note per paper under Obsidian `Papers/`,
+- synthesize those notes into `Knowledge/` notes,
+- verify that a whole Zotero collection has complete canonical note coverage,
+- visualize the literature structure as a default Obsidian canvas.
+
+## Core stance
+
+- Zotero owns **collection, metadata, attachments, PDF full text, annotations**.
+- Obsidian owns **durable reading notes, project-facing literature knowledge, cross-note links, synthesis**.
+- Prefer **one canonical paper note per paper**.
+- Prefer **filesystem-first Obsidian notes**; do not require Obsidian MCP.
+- Use this as an **explicit, agent-first skill** for Zotero ingestion.
+- Prefer **`Papers/` + `Knowledge/`** as the default bridge targets.
+- Prefer **`Maps/literature.canvas`** as the default graph artifact.
+- Prefer a strong review schema inside each canonical paper note:
+ - `Claim`
+ - `Method`
+ - `Evidence`
+ - `Limitation`
+ - `Direct relevance to repo`
+ - `Relation to other papers`
+- Treat some Zotero `webpage` items as valid literature sources when they still expose useful metadata, attachment text, or fulltext.
+
+## Default workflow
+
+1. Resolve the current project via `$obsidian-project-memory`.
+ - If the repo is already bound, use the existing vault.
+ - If it looks like a research repo but is not bound, bootstrap it first.
+2. Read Zotero items from the target collection/query.
+ - If the user asked for a full collection pass, prefer full coverage over a small representative subset.
+ - If the transport path through MCP is failing but a local `zotero-mcp` source checkout or Python environment is available, use that local fallback to read metadata/fulltext instead of stopping the workflow.
+3. For each paper:
+ - get metadata,
+ - get full text when available,
+ - get annotations/notes when helpful,
+ - create or update the canonical paper note in `Papers/`.
+4. Add project-facing structure to each paper note:
+ - claim,
+ - research question,
+ - method,
+ - evidence,
+ - strengths,
+ - limitation,
+ - direct relevance to repo,
+ - relation to other papers,
+ - links to related papers and the best matching `Knowledge/` notes.
+5. Synthesize the batch into durable `Knowledge/` notes such as:
+ - `Knowledge/Literature-Overview.md`
+ - `Knowledge/Method-Families.md`
+ - `Knowledge/Research-Gaps.md`
+ only when the synthesis is stable enough to deserve a canonical note.
+6. If the request is collection-scoped, update a durable inventory note that records:
+ - collection size,
+ - item triage,
+ - item -> canonical note mapping,
+ - current coverage count such as `16 / 16`.
+ - use the canonical inventory path `Knowledge/Zotero-Collection-{collection-slug}-Inventory.md`.
+7. Refresh `Maps/literature.canvas`.
+ - Prefer semantic filtering and edge thinning.
+ - Prefer argument-map structure (`paper + claim + method + gap`) over dense all-to-all paper links.
+ - If needed, keep a second lightweight display canvas such as `Maps/literature-main.canvas`.
+8. If the user also wants writing deliverables, update:
+ - `Writing/literature-review.md`
+ - `Writing/comparison-matrix.md`
+9. Update today's `Daily/` note and repo-local project memory.
+10. Run a deterministic verification pass after batch ingestion or schema refactors.
+
+## Default outputs
+
+- `Papers/*.md` - one detailed reading note per paper
+- `Knowledge/*.md` - durable literature synthesis notes when the batch yields stable knowledge
+- `Knowledge/Zotero-Collection-{collection-slug}-Inventory.md` - when the source is a concrete Zotero collection and coverage tracking matters
+- `Maps/literature.canvas` - default visual literature graph
+- `Maps/literature-main.canvas` - optional lightweight presentation graph after semantic filtering
+- `Writing/literature-review.md` - when the user asks for synthesis
+- `Writing/comparison-matrix.md` - when cross-paper comparison is useful
+
+## Safety rules
+
+- Do not dump raw full text into Obsidian.
+- Do not create a new note if the canonical paper note already exists.
+- Do not create `Concepts/` or `Datasets/` trees by default.
+- Do not treat `Experiments/`, `Results/`, or `Writing/` as the default bridge target during Zotero ingestion.
+- Do not force every relationship into a new note; use frontmatter fields, sections, wikilinks, and `linked_knowledge` first.
+- If a relationship is uncertain, record it in the note body or `Daily/` instead of manufacturing durable structure.
+- Do not stop at a representative subset if the user explicitly asked to check **all** papers in a collection.
+- Do not leave schema drift unresolved across notes in the same collection after a batch pass.
+
+## References
+
+Load only what is needed:
+- `references/WORKFLOW.md` - end-to-end Zotero -> Obsidian procedure
+- `references/PAPER-NOTE-SCHEMA.md` - detailed paper note fields and sections
+- `references/COLLECTION-INVENTORY-SCHEMA.md` - canonical inventory note naming and mapping contract
+- `references/LOCAL-ZOTERO-FALLBACK.md` - local fallback runbook when MCP transport fails
+- `examples/example-collection-inventory.md` - example inventory note
+- `scripts/verify_paper_notes.py` - deterministic schema, zotero-key, and inventory consistency check
+- `../obsidian-literature-workflow/references/PAPER-NOTE-SCHEMA.md` - shared note schema
+- `../obsidian-literature-workflow/references/CANVAS-WORKFLOW.md` - default literature canvas refresh rules
diff --git a/.agents/skills/zotero-obsidian-bridge/examples/example-collection-inventory.md b/.agents/skills/zotero-obsidian-bridge/examples/example-collection-inventory.md
new file mode 100644
index 0000000..40226a7
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/examples/example-collection-inventory.md
@@ -0,0 +1,27 @@
+---
+type: zotero-collection-inventory
+collection: Cross Subject
+collection_slug: cross-subject
+source: zotero
+coverage_expected: 3
+coverage_actual: 3
+updated: 2026-03-18T00:00:00Z
+---
+
+# Cross Subject Inventory
+
+## Coverage Summary
+- Expected items: 3
+- Canonical notes: 3 / 3
+
+## Item Mapping
+| Zotero Key | Item Title | Canonical Note | Status |
+|---|---|---|---|
+| ABCDEFGH | Shared Latent Geometry for Cross-Subject Speech Decoding | Papers/Shared-Latent-Geometry.md | covered |
+| HGFEDCBA | Domain Adapters for Neural Transfer | Papers/Domain-Adapters.md | covered |
+| ZXCVBNML | Contrastive Transfer under Low Resource Conditions | Papers/Contrastive-Transfer.md | covered |
+
+## Triage
+- fully covered: 3
+- bridge-only: 0
+- needs-review: 0
diff --git a/.agents/skills/zotero-obsidian-bridge/references/COLLECTION-INVENTORY-SCHEMA.md b/.agents/skills/zotero-obsidian-bridge/references/COLLECTION-INVENTORY-SCHEMA.md
new file mode 100644
index 0000000..759803d
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/references/COLLECTION-INVENTORY-SCHEMA.md
@@ -0,0 +1,49 @@
+# Collection Inventory Schema
+
+Use this note when a Zotero collection is imported or audited at collection scope.
+
+## Canonical path
+
+```text
+Knowledge/Zotero-Collection-{collection-slug}-Inventory.md
+```
+
+## Frontmatter
+
+```yaml
+---
+type: zotero-collection-inventory
+collection: Cross Subject
+collection_slug: cross-subject
+source: zotero
+coverage_expected: 16
+coverage_actual: 16
+updated: 2026-03-18T00:00:00Z
+---
+```
+
+## Required sections
+
+```markdown
+# Cross Subject Inventory
+
+## Coverage Summary
+- Expected items: 16
+- Canonical notes: 16 / 16
+
+## Item Mapping
+| Zotero Key | Item Title | Canonical Note | Status |
+|---|---|---|---|
+| ABCDEFGH | Example title | Papers/Example-Paper.md | covered |
+
+## Triage
+- fully covered
+- skipped or bridge-only notes
+- items that still need full notes
+```
+
+## Rules
+- Keep one durable inventory note per collection slug.
+- `Canonical Note` should use project-relative note paths.
+- `Status` should use a small vocabulary such as `covered`, `bridge-only`, `skipped`, `needs-review`.
+- Update coverage counts whenever the collection is batch-processed again.
diff --git a/.agents/skills/zotero-obsidian-bridge/references/LOCAL-ZOTERO-FALLBACK.md b/.agents/skills/zotero-obsidian-bridge/references/LOCAL-ZOTERO-FALLBACK.md
new file mode 100644
index 0000000..5bed8f5
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/references/LOCAL-ZOTERO-FALLBACK.md
@@ -0,0 +1,22 @@
+# Local Zotero Fallback
+
+Use this runbook when the Zotero MCP transport is unavailable but the user still wants a collection-level pass.
+
+## Preconditions
+
+- A local `zotero-mcp` checkout or installed Python package is available.
+- The local environment can read metadata/fulltext without mutating the Zotero library.
+
+## Default fallback sequence
+
+1. Confirm that MCP transport is the failing layer, not the collection query itself.
+2. Switch to the local Python-backed path for metadata and fulltext retrieval.
+3. Continue creating or updating canonical paper notes.
+4. Run `verify_paper_notes.py` before closing the batch.
+
+## Report explicitly
+
+Always state:
+- that MCP transport failed,
+- that the workflow continued through local fallback,
+- what remained unavailable, if anything.
diff --git a/.agents/skills/zotero-obsidian-bridge/references/PAPER-NOTE-SCHEMA.md b/.agents/skills/zotero-obsidian-bridge/references/PAPER-NOTE-SCHEMA.md
new file mode 100644
index 0000000..1171aab
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/references/PAPER-NOTE-SCHEMA.md
@@ -0,0 +1,81 @@
+# Zotero-Sourced Paper Note Schema
+
+## Frontmatter
+
+```yaml
+---
+type: paper
+project: project-slug
+title: "Paper Title"
+canvas_visibility: hidden
+authors:
+ - Author A
+ - Author B
+year: 2026
+venue: "NeurIPS"
+doi: "10.xxxx/xxxxx"
+url: "https://doi.org/..."
+citekey: "author2026paper"
+zotero_key: "ABCDEFGH"
+status: read
+keywords:
+ - subject-invariance
+ - contrastive-learning
+concepts:
+ - shared geometry
+methods:
+ - contrastive pretraining
+subfield: speech-transfer
+related_papers:
+ - "Papers/Another-Paper"
+linked_knowledge:
+ - "Knowledge/Literature-Overview"
+ - "Knowledge/Method-Families"
+argument_claims:
+ - "Shared geometry exists but is fragile"
+argument_methods:
+ - "Geometry-aware transfer"
+argument_gaps:
+ - "Still needs speech-specific validation"
+paper_relationships:
+ - "Papers/Another-Paper::complements"
+updated: 2026-03-16T00:00:00Z
+---
+```
+
+## Sections
+
+```markdown
+# Paper Title
+
+## Claim
+
+## Research question
+
+## Method
+
+## Evidence
+
+## Strengths
+
+## Limitation
+
+## Direct relevance to repo
+
+## Relation to other papers
+
+## Knowledge links
+
+## Optional downstream hooks
+- Writing:
+```
+
+## Rules
+
+- `related_papers` and `linked_knowledge` should prefer project-relative note paths.
+- `paper_relationships` should record explicit semantic edges only when they are stable enough to support graph construction.
+- `concepts` and `methods` may remain plain strings; do not create dedicated notes by default.
+- Keep `Direct relevance to repo` concrete and actionable.
+- Prefer one durable canonical note per paper; update in place instead of making sibling notes.
+- If the user asked for a full collection pass, normalize the schema across the entire covered set before closing the task.
+- Treat Zotero `webpage` items as acceptable inputs when attachment text or fulltext is still available and useful.
diff --git a/.agents/skills/zotero-obsidian-bridge/references/WORKFLOW.md b/.agents/skills/zotero-obsidian-bridge/references/WORKFLOW.md
new file mode 100644
index 0000000..605a408
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/references/WORKFLOW.md
@@ -0,0 +1,83 @@
+# Zotero -> Obsidian Workflow
+
+## 1. Resolve the project
+
+1. Run the Obsidian project-memory detect flow.
+2. If the repo is bound, use its `Research/{project-slug}/` vault root.
+3. If not bound but clearly a research repo, bootstrap first.
+
+## 2. Read from Zotero
+
+Preferred read path per paper:
+1. `zotero_get_item_metadata`
+2. `zotero_get_item_fulltext`
+3. `zotero_get_annotations`
+4. `zotero_get_notes`
+
+Use metadata + abstract as the minimum fallback when PDF full text is unavailable.
+If the MCP transport path is broken but a local `zotero-mcp` source checkout is available, use the local Python fallback to call the same metadata/fulltext functions instead of aborting.
+Treat some Zotero `webpage` items as valid literature entries when they still expose meaningful metadata or fulltext.
+
+## 3. Create/update the canonical paper note
+
+Canonical destination:
+- `Papers/{normalized-title-or-citekey}.md`
+
+Update instead of duplicating when the note already exists.
+
+## 4. Detailed reading note requirements
+
+Each durable paper note should contain:
+- claim
+- research question / problem
+- method
+- evidence
+- strengths
+- limitation
+- direct relevance to repo
+- links to related papers and the best matching `Knowledge/` notes
+
+## 5. Synthesize the stable literature knowledge
+
+After a batch import, prefer agent-first synthesis into `Knowledge/`:
+1. update `Knowledge/Literature-Overview.md` when the batch yields a stable overview
+2. update `Knowledge/Method-Families.md` when method clusters are clear
+3. update `Knowledge/Research-Gaps.md` when open problems or tensions are stable enough to keep
+4. if the source is a named Zotero collection, update a durable inventory note that records:
+ - collection size,
+ - triage buckets,
+ - collection item -> canonical note mapping,
+ - current coverage such as `16 / 16`
+
+## 6. Refresh the default literature canvas
+
+After batch note creation or major note updates:
+1. rebuild `Maps/literature.canvas`
+2. ensure core paper notes have meaningful wikilinks into `Knowledge/`
+3. keep the graph lightweight and project-facing
+4. prefer semantic filtering and edge thinning over dense all-to-all paper links
+5. prefer `paper + claim + method + gap` argument-map structure for the main graph
+6. add `Maps/literature-main.canvas` only when a second lightweight display graph is genuinely useful
+
+## 7. Push downstream only when justified
+
+- during Zotero ingestion, default to `Papers/` plus `Knowledge/`
+- update `Writing/` only when the user asks for a review, comparison, or draft-facing synthesis
+- treat `Experiments/` and `Results/` as later project workflows, not default Zotero-import targets
+
+## 8. Verify before closing
+
+After batch ingestion or schema refactors:
+1. verify every expected paper has a canonical note
+2. verify `zotero_key` coverage matches the imported collection
+3. verify all covered notes use the same canonical section schema
+4. update the project `Daily/` note and project memory with what changed
+
+Recommended verification command:
+
+```bash
+python3 "${CLAUDE_PLUGIN_ROOT}/skills/zotero-obsidian-bridge/scripts/verify_paper_notes.py" \
+ --papers-dir "/absolute/path/to/Papers" \
+ --expected-zotero-keys "KEY1,KEY2,KEY3" \
+ --inventory-note "/absolute/path/to/Knowledge/Zotero-Collection-collection-slug-Inventory.md"
+```
diff --git a/.agents/skills/zotero-obsidian-bridge/scripts/verify_paper_notes.py b/.agents/skills/zotero-obsidian-bridge/scripts/verify_paper_notes.py
new file mode 100755
index 0000000..d60e020
--- /dev/null
+++ b/.agents/skills/zotero-obsidian-bridge/scripts/verify_paper_notes.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+"""Verify Obsidian paper-note schema, Zotero-key coverage, and optional inventory consistency."""
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from pathlib import Path
+
+REQUIRED_HEADINGS = (
+ "## Claim",
+ "## Method",
+ "## Evidence",
+ "## Limitation",
+ "## Direct relevance to repo",
+ "## Relation to other papers",
+)
+
+REQUIRED_FRONTMATTER_FIELDS = (
+ "type",
+ "project",
+ "title",
+ "zotero_key",
+ "linked_knowledge",
+ "paper_relationships",
+ "updated",
+)
+
+ZOTERO_KEY_RE = re.compile(r'^zotero_key:\s*"?([A-Z0-9]+)"?', re.MULTILINE)
+FRONTMATTER_RE = re.compile(r'^---\n(.*?)\n---\n', re.DOTALL)
+FIELD_RE_TEMPLATE = r'^{}:\s*'
+INVENTORY_ROW_RE = re.compile(r'^\|\s*([A-Z0-9]+)\s*\|\s*(.*?)\|\s*(.*?)\|\s*(.*?)\|\s*$')
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Verify paper-note schema and optional Zotero-key / inventory coverage."
+ )
+ parser.add_argument("--papers-dir", required=True, help="Directory containing canonical paper notes.")
+ parser.add_argument(
+ "--expected-zotero-keys",
+ default="",
+ help="Comma-separated Zotero keys expected to be covered.",
+ )
+ parser.add_argument(
+ "--inventory-note",
+ default="",
+ help="Optional collection inventory note to cross-check against canonical paper notes.",
+ )
+ parser.add_argument(
+ "--strict-missing-zotero-key",
+ action="store_true",
+ help="Treat notes without zotero_key as errors. Default behavior skips them.",
+ )
+ return parser.parse_args()
+
+
+def load_expected_keys(raw_keys: str) -> list[str]:
+ if not raw_keys.strip():
+ return []
+ return [key.strip() for key in raw_keys.split(",") if key.strip()]
+
+
+def extract_frontmatter(text: str) -> str:
+ match = FRONTMATTER_RE.match(text)
+ return match.group(1) if match else ""
+
+
+def missing_frontmatter_fields(frontmatter: str) -> list[str]:
+ missing: list[str] = []
+ for field in REQUIRED_FRONTMATTER_FIELDS:
+ if not re.search(FIELD_RE_TEMPLATE.format(re.escape(field)), frontmatter, re.MULTILINE):
+ missing.append(field)
+ return missing
+
+
+def collect_note_status(
+ papers_dir: Path,
+ strict_missing_zotero_key: bool,
+) -> tuple[dict[str, str], list[str], list[str]]:
+ key_to_file: dict[str, str] = {}
+ issues: list[str] = []
+ skipped_without_key: list[str] = []
+
+ for path in sorted(papers_dir.glob("*.md")):
+ text = path.read_text(encoding="utf-8")
+ frontmatter = extract_frontmatter(text)
+ match = ZOTERO_KEY_RE.search(text)
+ if not match:
+ skipped_without_key.append(path.name)
+ if strict_missing_zotero_key:
+ issues.append(f"{path.name}: missing zotero_key")
+ continue
+
+ zotero_key = match.group(1)
+ if zotero_key in key_to_file:
+ issues.append(
+ f"{path.name}: duplicate zotero_key {zotero_key} also used by {key_to_file[zotero_key]}"
+ )
+ key_to_file[zotero_key] = path.name
+
+ missing_headings = [heading for heading in REQUIRED_HEADINGS if heading not in text]
+ if missing_headings:
+ issues.append(f"{path.name}: missing headings -> {', '.join(missing_headings)}")
+
+ missing_fields = missing_frontmatter_fields(frontmatter)
+ if missing_fields:
+ issues.append(f"{path.name}: missing frontmatter fields -> {', '.join(missing_fields)}")
+
+ return key_to_file, issues, skipped_without_key
+
+
+def parse_inventory_note(inventory_path: Path) -> tuple[dict[str, str], list[str]]:
+ text = inventory_path.read_text(encoding="utf-8")
+ mapping: dict[str, str] = {}
+ issues: list[str] = []
+ in_table = False
+
+ for line in text.splitlines():
+ if line.strip() == "## Item Mapping":
+ in_table = True
+ continue
+ if in_table and line.startswith("## "):
+ break
+ if not in_table or not line.startswith("|"):
+ continue
+ if "---" in line:
+ continue
+ match = INVENTORY_ROW_RE.match(line)
+ if not match:
+ continue
+ key, _title, note_path, _status = [part.strip() for part in match.groups()]
+ if key in mapping:
+ issues.append(f"inventory: duplicate zotero key {key}")
+ mapping[key] = note_path
+
+ if not mapping:
+ issues.append("inventory: no item mapping rows found")
+ return mapping, issues
+
+
+def main() -> int:
+ args = parse_args()
+ papers_dir = Path(args.papers_dir).expanduser()
+ expected_keys = load_expected_keys(args.expected_zotero_keys)
+
+ if not papers_dir.exists():
+ print(f"ERROR: papers dir not found: {papers_dir}")
+ return 1
+
+ note_files = list(papers_dir.glob("*.md"))
+ key_to_file, issues, skipped_without_key = collect_note_status(
+ papers_dir, args.strict_missing_zotero_key
+ )
+
+ print(f"Papers dir: {papers_dir}")
+ print(f"Paper notes scanned: {len(note_files)}")
+ print(f"Notes with zotero_key: {len(key_to_file)}")
+ if skipped_without_key:
+ print(f"Notes skipped without zotero_key: {len(skipped_without_key)}")
+
+ if expected_keys:
+ missing = [key for key in expected_keys if key not in key_to_file]
+ extras = sorted(set(key_to_file) - set(expected_keys))
+ print(f"Expected Zotero keys: {len(expected_keys)}")
+ print(f"Covered Zotero keys: {len(expected_keys) - len(missing)} / {len(expected_keys)}")
+ if missing:
+ issues.append(f"Missing expected keys: {', '.join(missing)}")
+ if extras:
+ print(f"Extra zotero_key notes: {', '.join(extras)}")
+
+ if args.inventory_note:
+ inventory_path = Path(args.inventory_note).expanduser()
+ if not inventory_path.exists():
+ issues.append(f"inventory note not found: {inventory_path}")
+ else:
+ inventory_mapping, inventory_issues = parse_inventory_note(inventory_path)
+ issues.extend(inventory_issues)
+ if inventory_mapping:
+ for key, note_path in inventory_mapping.items():
+ expected_name = key_to_file.get(key)
+ if expected_name is None:
+ issues.append(f"inventory: key {key} not found in paper notes")
+ continue
+ if Path(note_path).name != expected_name:
+ issues.append(
+ f"inventory: key {key} points to {note_path}, expected file ending {expected_name}"
+ )
+ missing_in_inventory = sorted(set(key_to_file) - set(inventory_mapping))
+ if missing_in_inventory:
+ issues.append(
+ f"inventory: missing keys present in paper notes -> {', '.join(missing_in_inventory)}"
+ )
+
+ if skipped_without_key:
+ print("Skipped note files without zotero_key:")
+ for name in skipped_without_key:
+ print(f"- {name}")
+
+ if issues:
+ print("\nISSUES:")
+ for issue in issues:
+ print(f"- {issue}")
+ return 1
+
+ print("\nOK: schema and coverage checks passed.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())