first commit
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: zotero-obsidian-bridge
|
||||
description: Use this skill when Zotero is the literature source of truth and the project KB should receive source notes under Sources/Papers plus project-linked synthesis in Knowledge and Writing.
|
||||
version: 0.3.0
|
||||
---
|
||||
|
||||
# Zotero Obsidian Bridge
|
||||
|
||||
Use this skill when papers live in Zotero and the project KB should receive project-local notes.
|
||||
|
||||
Default flow:
|
||||
|
||||
```text
|
||||
Zotero -> Sources/Papers -> Knowledge -> Writing -> Maps/literature.canvas
|
||||
```
|
||||
|
||||
Rules:
|
||||
- one canonical paper note per paper under `Sources/Papers/`
|
||||
- literature synthesis goes to `Knowledge/`
|
||||
- writing-oriented outputs go to `Writing/`
|
||||
- `Maps/literature.canvas` is the default derived graph artifact
|
||||
- update `_system/registry.md`, `02-Index.md`, and today's `Daily/` after substantial ingestion
|
||||
|
||||
## Evidence extraction requirement
|
||||
|
||||
Core papers and papers with reusable claims should include an evidence record. Abstract-only or placeholder notes may stay in `To-Read` until there is enough evidence to extract a reusable claim.
|
||||
|
||||
```md
|
||||
## Evidence Record
|
||||
|
||||
Evidence ID:
|
||||
Source:
|
||||
Source type: full paper | preprint | dataset | experiment artifact | project note | abstract-only | webpage placeholder
|
||||
Supports:
|
||||
Contradicts:
|
||||
Method / dataset / metric:
|
||||
Limitation:
|
||||
Project relevance:
|
||||
Claim strength: speculative | observed | supported | strong
|
||||
```
|
||||
|
||||
Use `../research-ideation/references/research-contract.md` as the shared contract for Evidence Records and claim strength definitions.
|
||||
|
||||
Before synthesis lands in `Knowledge/` or `Writing/`, apply the shared Claim Promotion Gate:
|
||||
- every promoted claim must point back to an Evidence Record ID,
|
||||
- source type must be strong enough for the claim,
|
||||
- abstract-only and webpage-placeholder items cannot support durable claims,
|
||||
- allowed wording and forbidden stronger wording must be preserved when the claim may later enter a report, manuscript, or rebuttal.
|
||||
|
||||
When synthesis lands in `Knowledge/`, link the supporting paper notes explicitly. Do not promote literature synthesis that cannot point back to source notes.
|
||||
@@ -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 | Sources/Papers/Shared-Latent-Geometry.md | covered |
|
||||
| HGFEDCBA | Domain Adapters for Neural Transfer | Sources/Papers/Domain-Adapters.md | covered |
|
||||
| ZXCVBNML | Contrastive Transfer under Low Resource Conditions | Sources/Papers/Contrastive-Transfer.md | covered |
|
||||
|
||||
## Triage
|
||||
- fully covered: 3
|
||||
- bridge-only: 0
|
||||
- needs-review: 0
|
||||
@@ -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 | Sources/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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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
|
||||
source_type: full paper
|
||||
claim_strength: observed
|
||||
keywords:
|
||||
- subject-invariance
|
||||
- contrastive-learning
|
||||
concepts:
|
||||
- shared geometry
|
||||
methods:
|
||||
- contrastive pretraining
|
||||
subfield: speech-transfer
|
||||
related_papers:
|
||||
- "Sources/Papers/Another-Paper"
|
||||
linked_knowledge:
|
||||
- "Knowledge/Literature Overview"
|
||||
- "Knowledge/Method Taxonomy"
|
||||
argument_claims:
|
||||
- "Shared geometry exists but is fragile"
|
||||
argument_methods:
|
||||
- "Geometry-aware transfer"
|
||||
argument_gaps:
|
||||
- "Still needs speech-specific validation"
|
||||
paper_relationships:
|
||||
- "Sources/Papers/Another-Paper::complements"
|
||||
updated: 2026-03-16T00:00:00Z
|
||||
---
|
||||
```
|
||||
|
||||
## Sections
|
||||
|
||||
```markdown
|
||||
# Paper Title
|
||||
|
||||
## Claim
|
||||
|
||||
## Research question
|
||||
|
||||
## Method
|
||||
|
||||
## Evidence
|
||||
|
||||
Use the shared Evidence Record shape from `research-ideation/references/research-contract.md`.
|
||||
|
||||
## 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 weak-source inputs by default. If they are abstract-only or placeholder pages, set `source_type: webpage placeholder` or `source_type: abstract-only`, keep `status: to-read`, and do not use them to support `Knowledge`, manuscript, or rebuttal claims.
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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 Zotero `webpage` items as weak-source entries unless they clearly expose full paper metadata and useful fulltext. Abstract-only pages and webpage placeholders may be kept for discovery, but they must remain `To-Read` or `weak-source` and cannot support `Knowledge` or manuscript claims until replaced by a full paper, preprint, or verified artifact.
|
||||
|
||||
## 3. Create/update the canonical paper note
|
||||
|
||||
Canonical destination:
|
||||
- `Sources/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 Taxonomy.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`
|
||||
|
||||
Apply the Claim Promotion Gate before creating or updating `Knowledge/`:
|
||||
- every stable claim must cite a canonical paper note and an Evidence Record ID,
|
||||
- `speculative` and `observed` claims stay as hypotheses, motivations, or open gaps,
|
||||
- abstract-only or webpage-placeholder items can appear in coverage notes but not as support for durable conclusions,
|
||||
- if the batch lacks enough evidence records, produce a warning/claim map instead of a polished synthesis.
|
||||
|
||||
## 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. preserve edge evidence strength where possible; do not create all-to-all paper links or stable-looking claim edges without source-note support
|
||||
|
||||
## 7. Push downstream only when justified
|
||||
|
||||
- during Zotero ingestion, default to `Sources/Papers/` plus `Knowledge/`
|
||||
- update `Writing/` only when the user asks for a review, comparison, or draft-facing synthesis and the promoted claims pass the Claim Promotion Gate
|
||||
- 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/Sources/Papers" \
|
||||
--expected-zotero-keys "KEY1,KEY2,KEY3" \
|
||||
--inventory-note "/absolute/path/to/Knowledge/Zotero-Collection-collection-slug-Inventory.md"
|
||||
```
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user