diff options
| author | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
|---|---|---|
| committer | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
| commit | 898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch) | |
| tree | 85c6ee5ad58b860144551184d4cf86b560c62b91 /.agents/skills/zotero-obsidian-bridge | |
| download | www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.tar.gz www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.zip | |
Diffstat (limited to '')
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()) |
