OKF Document Bundles, Agent Navigation & Mutation Invariants¶
Zocrates standardizes academic knowledge on the Open Knowledge Format (OKF v0.2) standard. Rather than locking converted documents behind proprietary database structures or transient service APIs, documents in Zocrates exist as transparent, human- and agent-readable Markdown bundles stored directly on disk.
This specification documents the bundle directory architecture, agent navigation protocols, cross-platform path semantics, and the exhaustive 12-path mutation lifecycle with its concurrency and data-preservation invariants.
1. Core Architecture & Philosophy: Format over Service¶
Coding agents (Claude, Gemini, Pi, Cursor, Copilot, ADK agents) require direct, high-fidelity access to research papers, technical reports, and notes. Zocrates adheres to the core tenets of the Open Knowledge Format:
- Just Markdown: Clean, GitHub-Flavored Markdown indexable by standard tools and renderable across environments.
- Just Files: Stored directly on the filesystem, versionable with Git, and inspectable without specialized SDKs.
- Just YAML Frontmatter: Structured metadata and trust signals stamped at the top of every document, enabling agents and code to evaluate relevance and credibility before reading the full text body.
- Co-Located Asset Bundles: Extracted figures, diagrams, and structured tables reside alongside the Markdown prose in a self-contained directory.
graph TD
subgraph "Zotero Storage"
A[Attachment PDF / HTML / MD / Notes]
end
subgraph "Conversion & Bundling (Zocrates Engine)"
A --> B[Document Converters: Docling / Datalab / MarkItDown]
B --> C["Item Bundle: corpus/{item_key}/"]
C --> D["document.md - OKF v0.2 Frontmatter"]
C --> E["images/ - PNG, JPG, SVG Figures"]
C --> F["tables/ - CSV, HTML Tables"]
C --> G["docling_document.json / datalab_document.json ASTs"]
end
subgraph "Catalog & Discovery Layer"
C --> H[("conversion_meta.db (SQLite WAL)")]
H --> I["index.md (Root Knowledge Catalog)"]
H --> J["AGENTS.md (Agent Navigation Guide)"]
C --> K["{item_key}/index.md (Item Asset Catalog)"]
end
subgraph "Agent Access Layer"
L[External Coding Agent / LLM] -->|1. Discovers Guide| J
L -->|2. Scans Catalog| I
L -->|3. Triages Metadata| D
L -->|4. Inspects Assets| K
L -->|5. Views High-Res Figures| E
end
2. OKF v0.1 vs. OKF v0.2: Agentic Decision Making¶
While basic descriptive metadata (type, title, description, tags) sufficed for OKF v0.1, OKF v0.2 introduces explicit decision and trust signals in YAML frontmatter. Because agent context windows are valuable, frontmatter enables LLMs and tools to make rapid filtering choices without consuming tokens on full document bodies:
| Signal Category | Frontmatter Field | Purpose for Agents |
|---|---|---|
| Identity & Types | type, item_key, parent_item_key |
Identifies document type (journalArticle, webpage, preprint) and parent-child Zotero linkages. |
| Bibliographic Metadata | title, authors, year, publication, doi, url |
Core citation facts for paper identification and citation tracking. |
| Collections | member_of_collections |
Zotero collection membership for topical filtering. |
| Source Provenance | source: {path, hash, content_type} |
Traceability to original disk attachments and SHA-256 integrity verification. |
| Converter Provenance | provenance: {converter_id, converter_version, converted_at} |
Identifies parsing engine (docling_local, datalab, markitdown), engine version, and conversion timestamp. |
| Quality & Trust | parse_quality_score, checkpoint_id |
Quantifiable OCR/layout quality metrics (e.g. 0.0 to 5.0 score from Datalab). |
Example OKF v0.2 Frontmatter Block:¶
---
type: journalArticle
okf_version: '0.2'
item_key: ARSFJKPA
parent_item_key: 9N8KQEZI
title: Differences between germline genomes of monozygotic twins
authors:
- Jonsson, Hakon
- Magnusdottir, Erna
- Eggertsson, Hannes P.
year: 2021
publication: Nature Genetics
doi: 10.1038/s41588-020-00755-1
url: https://www.nature.com/articles/s41588-020-00755-1
member_of_collections:
- name: Genomics
collection_key: CJHVF5G8
source:
path: ../../../storage/ARSFJKPA/Jonsson et al. - 2021.pdf
hash: 45c127e5da7132b0bf8c03674e62743d14fcf2f65aa4d7e02f4f868d260417ab
content_type: application/pdf
provenance:
converter_id: docling_local
converter_version: 2.120.1-fast
converted_at: '2026-08-18T14:42:07.263641+00:00'
parse_quality_score: 4.85
---
3. Anatomy of a Zocrates Item Bundle¶
All converted documents reside in <zotero-data-dir>/zocrates-data/corpus/:
corpus/
├── index.md # Root Knowledge Base Catalog (all documents)
├── AGENTS.md # Agent Navigation Guide & Protocol
├── conversion_meta.db # SQLite metadata database (authoritative, WAL mode)
└── <item_key>/ # Item-centric bundle directory (attachment item key)
├── document.md # Converted Markdown with OKF v0.2 YAML frontmatter
├── toc.json # Machine-parseable Table of Contents & asset index
├── toc.md # Agent-readable overview with Abstract and ToC
├── item_relations.md # Related literature neighborhood (0 bytes if orphan)
├── index.md # Item-level catalog describing metadata, assets, and relations
├── images/ # Extracted figures and rasterized diagrams (PNG, JPG, SVG)
│ ├── picture_000.png
│ └── picture_001.png
├── tables/ # Structured table extractions (CSV, HTML)
│ ├── table_000.csv
│ └── table_000.html
├── image_captions.json # Sidecar caching VLM synthetic figure captions
└── docling_document.json # Parser AST sidecar (offline debugging only)
First-Class Child Note OKF Bundles¶
In addition to PDF, HTML, and Markdown files, user notes in Zotero (reading reflections, critiques, literature summaries) are materialized as first-class OKF bundles:
corpus/<note_key>/
├── document.md # YAML frontmatter + converted structured Markdown
├── toc.json # Line-offset boundaries and section assets
├── toc.md # Human/agent readable Table of Contents
├── item_relations.md # Related paper links
└── index.md # Note asset catalog
- HTML Conversion & Structural Fidelity: Notes are converted from HTML to Markdown via BeautifulSoup, preserving headings (
<h1>–<h6>), blockquotes (<blockquote>), clean pipe tables, code blocks (<pre><code>), and lists. - Strictly Non-LLM Summaries: Notes follow a zero-LLM summarization policy. Notes with fewer than 100 plain-text words skip summary generation (
abstract = None). Notes with ≥ 100 words use a deterministic extractive lead-paragraph method (abstract_source = "extractive") taking the first non-heading paragraph up to 300 characters (<5ms, zero API cost). - Staleness Tracking & On-Demand Materialization: If an agent requests
get_document_tocorread_documentfor a note that is not yet bundled,_resolve_bundle_dirmaterializes the bundle dynamically. The SHA-256 hash of raw note HTML is recorded assource_hashinconversion_meta.db. If the hash diverges, the bundle refreshes atomically.
Attachment-Free Metadata OKF Bundles¶
Items in Zotero lacking file attachments (PDF/HTML) or child notes—such as papers imported via DOI, BibTeX, Crossref, OpenAlex, or browser translators—are materialized as first-class OKF bundles:
corpus/<parent_item_key>/
├── document.md # YAML frontmatter + abstract + isolated overview notice
├── toc.json # Line-offset boundaries and section boundaries
├── toc.md # Human/agent readable Table of Contents
├── item_relations.md # Related paper links (0 bytes if unconnected)
└── index.md # Metadata asset catalog
- Virtual Source Paths: Frontmatter and SQLite metadata record
source_path = "zotero://item/<parent_item_key>"andcontent_type = "text/zotero-metadata", seamlessly bypassed by disk file converters while participating fully in catalog generation, graph relations, and vector retrieval. - Abstract Sanitization & Callout Isolation: JATS XML tags (
<jats:*>) are cleanly stripped, raw markdown headings in abstracts are converted to bold text to preserve outline hierarchy, and user-facing notices (> **Notice**: This Zotero entry does not have an attached full-text document...) are isolated under## Document Overviewoutside## Abstractto shield vector embeddings from boilerplate tokens. - Post-Ingest PDF Supersession: When a user later attaches a PDF to an existing metadata-only paper, the parent metadata bundle is purged across disk, SQLite metadata, and vector datastores strictly after the child attachment is successfully converted and indexed.
4. Agent Navigation Protocol & Discovery¶
External coding agents interact with document bundles directly via standard filesystem tools (read, grep, find, ls).
| Dimension | Plain Folder Path Access | With Navigation Protocol & Hybrid Retrieval |
|---|---|---|
| Integration Overhead | Zero. Standard file tools only. | Minimal. Guided by AGENTS.md. |
| Small Corpus (< 200 items) | Efficient. Scans index.md, filters candidate frontmatters, reads target documents. |
Rapid document resolution in 1–2 turns. |
| Large Corpus (1,000+ items) | High token consumption, slow recursive grepping. | Optimal. Hybrid RAG retrieves candidate keys; agent reads specific OKF bundles on disk. |
| Figure & Multimodal Access | Manual file discovery. | Streamlined. Relative links in document.md and synthetic captions make figures directly discoverable. |
| Portability | Fragile if paths are host-absolute. | Fully Portable. Relative links (../../../storage/...) preserve validity across systems and containers. |
The 5-Step Agent Navigation Protocol¶
sequenceDiagram
autonumber
actor Agent as Coding Agent / LLM
participant RootIdx as corpus/index.md
participant DocMD as item_key/document.md
participant TocMD as item_key/toc.md
participant ItemIdx as item_key/index.md
participant RelMD as item_key/item_relations.md
participant Assets as Assets (images and tables)
Agent->>RootIdx: 1. Read catalog table (Item Key, Title, Type, Chars)
Note over Agent: Select candidate item_keys based on Title and Type
Agent->>TocMD: 2. Triage Table of Contents and Abstract
Note over Agent: Identify relevant section IDs and line ranges
Agent->>DocMD: 3. Targeted section read / search
Note over Agent: Read text, semantic figure blocks, and captions
opt High-Resolution Asset Inspection
Agent->>ItemIdx: 4a. Check item asset catalog
Agent->>Assets: 4b. Read image with multimodal tool or load table CSV
end
opt Literature Graph and Neighborhood Traversal
Agent->>RelMD: 5a. Read verified related papers
Agent->>DocMD: 5b. Jump directly to neighbor document.md
end
- Step 1: Catalog Discovery: Read
corpus/index.md. The catalog table contains| Item Key | Title | Type | Converter | Converted At | Source Path | Chars |. Candidate keys are selected without traversing subdirectories. - Step 2: Frontmatter & ToC Triage: Open
<item_key>/toc.md(or inspect the initial 30–50 lines of<item_key>/document.md). Review the abstract and section structure to identify exact line ranges and section IDs before reading the full document. - Step 3: Targeted Content Reading: Read specific sections or search for terms within
<item_key>/document.md. Documents feature ATX headers, structured<figure data-type="table">containers, and<figure data-type="image">blocks with<synthetic_caption>sections. - Step 4: Asset Inspection: When figures, diagrams, or tabular data require inspection, consult
<item_key>/index.mdfor relative links, inspect images inimages/via multimodal tools, or parse CSVs intables/. - Step 5: Relation & Literature Graph Traversal: If adjacent prior or follow-up work is needed, open
<item_key>/item_relations.md. It lists related papers with relative links ([KEY - Title](../KEY/document.md)), relation categories (human_curated,hybrid_search,shared_tags), and similarity scores. If the file is 0 bytes, the paper has no corroborated relations in the library (an unconnected paper).
Strict Guardrails for Agents¶
- Context-Toxic AST Sidecars: High-fidelity parsers (Docling, Datalab) store full JSON ASTs (
docling_document.json,datalab_document.json) that routinely exceed 5MB to 15MB. Agents must never glob or read parser AST JSON files during routine exploration. By contrast,toc.json,toc.md,item_relations.md, andimage_captions.jsonare lightweight, safe index files designed for agent reading. - Table Cell Pipe Corruption: Academic titles frequently contain pipe characters (
|) or newlines. All titles serialized ingenerate_root_index_mdare sanitized (replace("|", "\\|").replace("\r", " ").replace("\n", " ")) to prevent corrupted Markdown tables. - Three-Tier Path Semantics Architecture:
- Database Tier (
conversion_meta.db): Source paths are normalized to storage-relativestorage/<item_key>/<filename>. Virtual note URIs (zotero://note/<key>) are preserved verbatim. - Root Catalog Tier (
corpus/index.md): Base iscorpus/. Relative links are derived deterministically as"../../" + stored_path(e.g.[Source Document](../../storage/<key>/<file>)). - Document Provenance Tier (
document.mdYAML Frontmatter): Base is<cache_dir>/<item_key>/. Relative paths are derived as"../../../" + stored_path(e.g.path: ../../../storage/<key>/<file>). - External Linked Files: Files residing outside Zotero's internal
storage/directory retain normalized absolute POSIX paths, avoiding fragile relative traversals across unrelated host directories. - Fallback Re-anchoring: If a foreign machine absolute path is encountered,
resolve_source_pathextracts the trailingstorage/<key>/<file>component and re-anchors it to the local Zotero profile directory.
5. Vector Retrieval vs. Structural Document Bundles¶
Zocrates enforces a strict division of labor between Vector/FTS Retrieval (ChromaDB + SQLite FTS5) and Structural Document Bundles (OKF):
graph TD
subgraph "1. Discovery & Semantic Routing"
Q[User Query across Library] --> VEC[ChromaDB Vector & FTS5 Search]
VEC -->|Matches candidate chunks| KEYS["Top Item Keys: 89X2ELQ6, ARSFJKPA"]
end
subgraph "2. Structural Deep Reading & Asset Access"
KEYS --> BUNDLE["Target OKF Bundle: corpus/{item_key}/"]
BUNDLE --> TOC["toc.md / toc.json: Abstract + Sections + Figures + Tables"]
TOC -->|Agent targets Section s6| SEC["Read complete section: 3.2 Attention"]
TOC -->|Agent targets Figure f2| FIG["Inspect visual diagram: images/figure-002.png"]
TOC -->|Agent targets Table t1| TBL["Load structured data: tables/table-001.csv"]
end
Discovery vs. Deep Reading Trade-offs¶
| Dimension | ChromaDB / Vector Store (Chunk Layer) | OKF Document Bundle (toc.md / document.md) |
|---|---|---|
| Primary Role | Discovery & Semantic Search across the entire library. | Structural Understanding & Deep Reading of targeted papers. |
| Granularity | Fragmented text chunks (~1,000–1,500 chars) with artificial overlaps. | Complete hierarchical sections, full tables, and linked high-res figures. |
| Contextual Integrity | Can lose narrative flow, premises, and equation definitions. | Preserves complete context, preceding equations, and narrative continuity. |
| Structure Awareness | Flat list of chunk matches without document outline hierarchy. | Exact heading hierarchy (H1 -> H2 -> H3) and sequential IDs (s0..., f1..., t1...). |
| Asset Delivery | Embedding models cannot deliver CSV files or high-resolution images. | Co-located tables/ (CSV/HTML) and images/ (PNG/SVG) accessible with exact relative paths. |
Vector search alone cannot reliably reconstruct entire multi-step derivations or complex methodology sections. Pre-generating toc.json and toc.md allows agents to read a compact outline (~150 tokens) and target specific section IDs (e.g. s6), figures (f1), or tables (t1) in a single surgical read.
6. Bundle Mutation Lifecycle & Master Inventory¶
All write operations inside corpus/<key>/ are governed by 13 distinct write paths:
| # | Writer Path | Trigger Condition & Frequency | Source Location | Files Touched | Mechanism & Lifecycle | Risk / Invariant Guardrail |
|---|---|---|---|---|---|---|
| 1 | Initial Conversion & Cache Save | Trigger: Real-time event observer or batch sync. When: autoConverter.js detects new attachment (event === 'add'), or batch conversion runs (POST /batch-convert). |
zocrates.rag.document_processor (_save_to_cache) |
document.mdtoc.json, toc.mdindex.mdRoot index.md, AGENTS.md |
Writes bundle files into corpus/<key>/; executes INSERT OR REPLACE INTO conversion_meta |
Explicit Columns: Must include keywords so re-saves never clear metadata.Uniform Birth: Universal materialization of item_relations.md (0 bytes if orphan). |
| 2 | Explicit Re-Conversion | Trigger: User action. When: User clicks "Re-convert with..." in item pane or calls POST /convert/{item_key} (force=True). |
zocrates.rag.document_processor (convert_with) |
Entire bundle directory | Staged conversion: shutil.rmtree(item_dir) → temp directory staging → atomic rename before calling _save_to_cache |
Re-Conversion Invariant: Re-conversion wipes bundle directory; _save_to_cache regenerates item_relations.md from surviving SQLite rows so neighbor links are restored immediately. |
| 3 | Lazy / In-Place Cache Conversion | Trigger: Agent action. When: Dialogue agent calls read_document or get_document_toc on an item not yet converted to Markdown. |
zocrates.rag.document_processor (get_or_convert) |
document.mdBundle assets |
Converts directly into existing item_dir without rmtree; calls _save_to_cache |
Preserves existing auxiliary files (e.g. image_captions.json); calls _save_to_cache to finalize. |
| 4 | Sync-Time Orphan Cleanup | Trigger: Library sync scan or startup staleness sweep. When: User clicks "Sync Library" ( POST /sync) or startup sweep runs, detecting an item key removed or trashed in zotero.sqlite. |
zocrates.rag.library_syncer (purge_item) |
Entire bundle directory, conversion_meta.db, vector store, root index |
Purges across all four datastores: processor.delete_cached() + store.delete_document() + processor.regenerate_root_index() |
Pre-Deletion Neighbor Regeneration: Query all affected neighbors from item_relations before deleting edges; batch-regenerate neighbor item_relations.md.Store-Parity Invariant across disk, metadata DB, and vector store. |
| 5 | Note Bundle Materialization | Trigger: Note edit or creation detected during sync scan. When: User clicks "Sync Library" ( POST /sync) or full sync runs, detecting a SHA-256 hash change in a note's HTML. |
zocrates.rag.document_processor (materialize_note_bundle) |
document.mdtoc.json, toc.mdindex.mditem_relations.md |
Staged note generation: detects edits via SHA-256 of Zotero HTML; writes to temp staging dir → atomic rename | Generate index.md and item_relations.md inside staging dir before swap.Explicit SQL column list includes keywords. |
| 6 | Metadata & Frontmatter Sweeps | Trigger: Startup initialization or full library sync. When: Exclusively on startup POST /register sweep, manual POST /sync (or /sync/stream), or CLI utility. Never runs during chat turns. |
zocrates.rag.frontmatter_refresher (refresh_bundle_frontmatter) |
document.mdtoc.json, toc.mdindex.md |
In-place frontmatter regeneration; ΔL line-shifting of chunk_meta; FTS5 Chunk 0 replacement; conversion_meta update |
build_frontmatter() accepts and preserves keywords; no-op check includes keywords to prevent silent erasure.Skip content_type == 'text/note'. |
| 7 | VLM Multimodal Figure Enrichment | Trigger: Post-conversion hook. When: Immediately following document conversion if uncaptioned images exist and VLM is enabled. |
zocrates.rag.vlm_captioner (enrich_document_bundle) |
document.md (body only)image_captions.json |
Rewrites document.md body to inject <figure> and <synthetic_caption> blocks; caches descriptions in image_captions.json |
Body-Only Invariant: Frontmatter is untouched. Called from 4 post-conversion sites (library_syncer incremental, full sync, POST /convert/{item_key}, batch sync). |
| 8 | Table of Contents Generator | Trigger: Conversion, line-shift, or missing ToC. When: Inside _save_to_cache, after frontmatter ΔL shift, or lazy call in toolkit.py. |
zocrates.rag.toc_generator (save_toc_files) |
toc.jsontoc.md |
Extracts section hierarchy, line offsets, and asset captions; writes atomic .tmp files → os.replace |
Writing .tmp followed by os.replace eliminates partial-read race conditions for concurrent agent readers. |
| 9 | Root & Agent Catalogs | Trigger: Bundle addition or deletion. When: End of _save_to_cache, delete_cached, POST /sync, or batch conversion. |
zocrates.rag.bundle_catalog (generate_root_index_md, generate_agents_md) |
Root index.mdRoot AGENTS.md |
Sweeps active bundles in corpus/ to compile unified library catalog and agent guidance |
Reads conversion_meta and bundle paths; writes atomic full catalogs at library root. |
| 10 | Lazy ToC Backfill | Trigger: Agent ToC request on unindexed bundle. When: Dialogue agent calls get_document_toc() or search_corpus() on an existing bundle where toc.json has not been generated yet. |
zocrates.rag.library_syncer, zocrates.agents.common.toolkit |
toc.jsontoc.mdindex.md |
On-demand generation triggered when dialogue agent requests ToC of a bundle missing ToC files | Uses shared toc_generator and generate_item_index_md routines to maintain layout parity. |
| 11 | Maintenance CLI Sweeps | Trigger: Manual operator invocation. When: Developer runs CLI scripts in terminal ( scripts/refresh_frontmatter.py). |
scripts/refresh_frontmatter.py, scripts/refresh_toc.py |
document.mdtoc.json, toc.mdindex.md |
Standalone CLI maintenance utilities | Delegate directly to paths 6 and 8; inherit all safety guardrails. |
| 12 | Real-Time Deletion Purge | Trigger: Real-time event observer. When: autoConverter.js detects event === 'trash' or event === 'delete' via Zotero.Notifier observer, firing POST /items/delete. |
zocrates.rag.library_syncer (purge_item), zocrates.fast_api_app (delete_items) |
Entire bundle directory, conversion_meta.db, vector store, root index |
Immediate atomic purge across all four datastores upon item trashing or deletion in Zotero UI | Enforce 4-datastore store parity; batch-regenerate neighbor item_relations.md; eliminate stale point-of-use lookups. |
| 13 | Metadata Bundle Materialization | Trigger: Ingestion or sync of attachment-free regular item. When: POST /items/ingest from Scholar Import or AutoConverter, full_sync scan encountering an item with no file attachments or child notes, or on-demand agent fallback in _resolve_bundle_dir. |
zocrates.rag.document_processor (materialize_item_metadata_bundle) |
document.mdtoc.json, toc.mdindex.mditem_relations.md |
Staged metadata bundle generation: writes to temp staging dir → atomic rename; registers conversion_meta with content_type="text/zotero-metadata" |
Atomic directory swap; JATS XML abstract sanitization; notice callout isolation; centralized post-ingest PDF supersession when attachments are subsequently converted. |
7. Trigger Conditions & Event-Driven Execution Model¶
Zocrates utilizes an event-driven architecture centered on Zotero's native Zotero.Notifier subsystem, supplemented by startup sweeps and on-demand agent invocations:
graph TD
subgraph "1. Real-Time Event Observer (Zotero.Notifier)"
A["User adds PDF/attachment in Zotero"] --> B["autoConverter.js (Notifier 'add' event)"]
B -->|"POST /convert/{item_key}"| C["Writer 1: _save_to_cache"]
C -->|If images present| D["Writer 7: enrich_document_bundle (VLM)"]
A2["User trashes/deletes item in Zotero"] --> B2["autoConverter.js (Notifier 'trash'/'delete' event)"]
B2 -->|POST /items/delete| C2["Writer 12: purge_item (4 Datastores)"]
end
subgraph "2. User-Triggered Sync & Re-Conversion"
E["User clicks 'Scan Library' (#sync-btn)"] -->|POST /sync| F["library_syncer.full_sync()"]
F -->|Deleted items detected| G["Writer 4: purge_item (Orphan Cleanup)"]
F -->|Edited note HTML detected| H["Writer 5: materialize_note_bundle"]
I["User clicks 'Re-convert with Docling'"] -->|"POST /convert/{item_key}" force=True| J["Writer 2: convert_with"]
end
subgraph "3. Startup & Registration Sweep"
K["Zotero UI launch (autoRegister)"] -->|POST /register| L["Writer 6: refresh_bundle_frontmatter sweep"]
K -->|POST /register| M["Writer 9: generate_root_index_md sweep"]
K -->|POST /register| L2["Writer 4: sync_metadata_staleness orphan purge"]
end
subgraph "4. On-Demand / Lazy Fallback"
N["Agent calls get_document_toc (missing)"] --> O["Writer 10: Lazy ToC Backfill"]
P["Agent calls read_document (missing)"] --> Q["Writer 3: get_or_convert"]
end
Detailed Trigger Breakdown:¶
- Real-Time Event-Driven Ingestion & Deletion (Writers 1, 7, 12):
autoConverter.jsregisters an observer:Zotero.Notifier.registerObserver(this, ["item"], "ZocratesAutoConverter", 10).- On
add, it verifies file availability on disk (waitForAttachmentFile), queues it, and dispatchesPOST /convert/{item_key}to the backend, followed by Writer 7 VLM enrichment if images are present. - On
trashordelete, it resolves child attachments and notes, then firesPOST /items/delete. The backend invokesLibrarySyncer.purge_item(key)to execute synchronous sequential purge with store parity across all 4 datastores. - User-Triggered Library Scan (Writers 4 & 5):
- User clicks "Scan Zotero library" (
#sync-btn) or runs a batch conversion dialog, triggeringPOST /sync. LibrarySyncer.full_sync()scanszotero.sqliteagainst all known keys:- Deleted or trashed items trigger Writer 4 (
purge_item) across all 4 datastores. - Notes with modified HTML hashes trigger Writer 5 (
materialize_note_bundle).
- Deleted or trashed items trigger Writer 4 (
- Zero background polling: updates occur strictly on user action or event hooks.
- Startup & Registration Sweep (Writers 4, 6 & 9):
- On Zotero launch or sidebar mount,
autoRegister()sendsPOST /register. - Writer 6 (
refresh_bundle_frontmatter) updates frontmatter only when bibliographic values diverge. - Writer 4 (
sync_metadata_staleness) detects and purges orphaned items deleted while the backend was offline. - Writer 9 compiles root
index.md. Never runs during chat turns. - On-Demand / Lazy Interactive Paths (Writers 2, 3, 10):
- Writer 2 (
convert_with) runs when the user explicitly re-converts via the item pane. - Writers 3 & 10 (
get_or_convert/ Lazy ToC) execute lazily when an agent queries an item whose Markdown or ToC files do not yet exist on disk.
8. System Invariants & Concurrency Rules¶
8.1. The Re-Conversion Invariant¶
convert_with() wipes the target bundle directory using shutil.rmtree(). Because _save_to_cache() immediately regenerates item_relations.md from surviving SQLite item_relations rows, the bundle is fully populated from creation, and index.md never contains a dangling reference to a missing relations file.
8.2. The Deletion & Neighbor Link Invalidation Invariant¶
When Paper A is deleted or moved to trash in Zotero, deleting A's directory removes A. However, any existing neighbor B whose item_relations.md linked to [A - Title](../A/document.md) now possesses a dead link.
To prevent dangling links:
- Enumerate all neighbors before removing edges:
- Delete edges from SQLite:
- Batch-regenerate
item_relations.mdfor all affected neighbors.
8.3. The Note Isolation & Staging Invariant¶
When a user edits a note:
materialize_note_bundle()stages the new note in a temporary directory (<note_dir>.tmp.<pid>).- Inside the staging directory, it writes
document.md,toc.json,toc.md,index.md, anditem_relations.md. - It atomically renames the staging directory over the existing directory using
os.replace(on POSIX) or atomic directory swap. - The explicit SQL column list for
INSERT OR REPLACE INTO conversion_metaincludeskeywordsto prevent nulling existing metadata.
8.4. The Keyword Preservation Invariant¶
When bibliographic metadata changes in Zotero (e.g. title corrected, authors added), refresh_bundle_frontmatter() rewrites the YAML frontmatter. build_frontmatter() accepts keywords: list[str] | None = None. refresh_bundle_frontmatter() loads keywords from conversion_meta.keywords (or parses them from the existing YAML header). The deterministic equality comparison (new_fm == old_fm) includes the keywords: block. If metadata has not changed, zero disk writes occur.
refresh_bundle_frontmatter() also verifies content_type. If content_type == 'text/note', it skips processing, preventing note frontmatter from being rewritten into an attachment schema.
8.5. The Four-Datastore Purge & Store-Parity Invariant¶
Whenever an item is purged—whether through real-time observer events (autoConverter.js → POST /items/delete), sync-time orphan detection in full_sync(), or startup sweeps in sync_metadata_staleness—LibrarySyncer.purge_item(item_key) executes synchronous sequential purge across all 4 datastores:
- Corpus Directory:
processor.delete_cached(item_key, update_root_index=False)removescorpus/<key>/viashutil.rmtree. - Metadata & Relation DB:
conversion_meta.dbrows inconversion_meta,conversion_failures, anditem_relationsare deleted, and affected neighboritem_relations.mdfiles are regenerated. - Vector & FTS Store:
store.delete_document(item_key)purges dense vector embeddings in ChromaDB and keyword index rows in SQLite FTS5 (chunks_ftsandchunk_meta). - Root Index Catalog:
processor.regenerate_root_index()recompiles rootindex.mdto reflect the updated corpus.
No item key may exist in one datastore while missing from another. Deletion from disk without vector purge produces phantom search hits; deletion from vectors without disk purge leaves orphaned bundles on the filesystem.
8.6. Query-Time Liveness Invariant¶
Physical purges ensure long-term hygiene, but concurrent chat turns require query-time liveness gating against zotero.sqlite at the point of use:
- Hybrid Corpus Retrieval (
search_corpus_core): - Candidate results returned by
syncer.store.search(...)are filtered againstsyncer.db.get_active_item_keys(list(cand_keys)). - Items residing in Zotero's
deletedItemstable (or having a trashed parent) are discarded immediately. - Bundle Directory Resolution (
_resolve_bundle_dir): - Before resolving or materializing any note or disk bundle directory,
_resolve_bundle_dircheckssyncer.db.is_item_active(item_key). - If the item is trashed or deleted in Zotero, it immediately returns
None. - Cached ToC Eviction (
get_document_toc): - When resolving a Table of Contents, if a match is found in
cached_tocsbutsyncer.db.is_item_active(resolved_key)indicates the document is no longer active, the entry is evicted fromcached_tocsin session state, and a clear error is returned.
8.7. Concurrency, Atomicity & File-Replacement Standards¶
- Atomic File Writes:
Files that may be read concurrently by dialogue agents (
item_relations.md,toc.json,toc.md) are never written using direct truncation (open(..., "w")). They follow the atomic replacement pattern: - SQLite Database Access:
conversion_meta.dbis maintained inPRAGMA journal_mode=WALwithtimeout=5.0s. Readers never block concurrent writers. - Dynamic ToC Count Isolation:
In
toolkit.py,**Related Items**: Nis evaluated and appended to the ToC string at call-time after retrieving the outline fromcached_tocs. It is never stored incached_tocs, preventing the outline filter from stripping it and eliminating turn-to-turn staleness.
9. Architectural Cross-References¶
- Document Conversion Architecture: For converter strategies, Datalab OCR, Docling fast mode, and image extraction pipelines, see Document Conversion Architecture.
- Embeddings & Hybrid Search: For vector indexing, ChromaDB collections, and SQLite FTS5 trigram search, see Embeddings & Hybrid Search.
- System Overview: For high-level frontend-backend architecture and multi-agent roles, see System Architecture.