A library for stores of YAML-frontmatter markdown documents. It is the storage layer under tisket, zettel, and almanac. Use it when you want structured data in markdown files in git, and you do not want to write the frontmatter parser, the link graph, or the composition rules again.
The crate has two halves. The document half parses and serializes one file. The store half composes many stores into one graph you can read from a single vantage point.
From the first published release onward:
cargo add mdstore-core --rename mdstoreOr in Cargo.toml:
[dependencies]
mdstore = { version = "0.5", package = "mdstore-core" }
serde = { version = "1", features = ["derive"] }The crate is mdstore-core and the import path is mdstore.
mdstore on crates.io belongs to an unrelated project, and a registry
name is first-come and permanent, so this crate took a qualified one.
[lib] name keeps the import path, so every use mdstore::… reads the
same and only the dependency line carries the registry name.
Document<T> holds typed frontmatter and a string body. The
frontmatter type is any Serialize + DeserializeOwned. parse reads
the YAML between the --- fences. serialize writes the canonical
form back.
use mdstore::{parse, serialize, Document};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Note {
title: String,
tags: Vec<String>,
}
fn main() -> mdstore::Result<()> {
let raw = "---\ntitle: Hello\ntags: [a, b]\n---\n\nbody text";
let doc: Document<Note> = parse(raw)?;
println!("{} {:?}", doc.frontmatter.title, doc.body);
let out: String = serialize(&doc)?;
let _: Document<Note> = parse(&out)?;
Ok(())
}The provenance module records who wrote each part of a body. A span
carries an origin: human, agent, or citation. A missing origin
means unknown, and nothing promotes it to human.
The markers are HTML comments, so any markdown reader ignores them:
<!-- prov agent:summary -->
The trial enrolled 214 patients across nine sites.
<!-- /prov -->parse_spans reads a body into spans. render_spans writes them back.
ends_open reports a body with a marker that no /prov closes.
A store is a directory with a stores.yml file. That file declares the
other stores this one may link into, under local aliases. A store sees
only the stores it declares, and the stores those declare in turn. This
makes a directed graph, and a cycle is allowed.
format: 2
shared: false
stores:
- alias: method
git: https://github.com/example/method-notes
rev: main
- alias: archive
path: ../archiveA declaration carries one source: path, git with an optional rev,
or blob. Set shared: true when other people clone the store.
StoresConfig::unshareable then reports a dependency path that only
resolves on the declaring machine.
StoreGraph::open walks that declaration into a closure of members.
A store's identity is its resolved source, not a name it gives itself,
so one store reached two ways stays one member and a cycle terminates.
An alias resolves through the alias table of the store that holds the
referring document, never through the vantage store's table.
Snapshot::load reads every document in the closure once and builds
the link graph over them. It answers forward, backlinks,
neighborhood, orphans, and missing.
~/.config/<tool>/config.yml holds one key, root_store: the user's
private store, the fallback vantage for a read that finds no store at
or above its working directory. A write never falls back.
Each consumer owns its file, named by the tool the user runs. A user configures tisket, zettel, or almanac, and never this library, so this library's name stays out of the config directory. Parsing, the format gate, and the atomic write live here, and the caller supplies its name.
For config.yml the path shape is fixed and the home directory comes
from the passwd database, never $HOME or XDG_CONFIG_HOME: every
environment channel is repo-settable (direnv, mise), and this file names
where a write can land. Consumers expose the resolution as one rule
through mdstore::resolve and set the file with their store root
subcommand.
registry.yml sits in the same per-tool directory, and its resolution
is weaker: MDSTORE_REGISTRY names it outright, and XDG_CONFIG_HOME
or $HOME supply the base. A repo can therefore set which registry
answers, and the registry decides which content a declared URL
resolves to. Tightening that to the rule above is open work.
A member store can be local or remote:
path— a directory on this machine.git— a bare clone in a per-URL cache slot, read at each consumer's declared rev through git objects. Two consumers that pin different revisions share one fetch, and neither overwrites the other. Only an explicit sync reaches the network. All of it runs in-process on gix: https and git:// over gix's own transports, and a local repository by reading its object database. No git process runs. An ssh URL is refused, because gix would spawn ssh for it; declare https.blob— an https prefix that publishes anindex.txt, synced into a cache directory by plain GET. No vendor CLI runs;s3://andgs://are refused.
The registry module holds local overrides. An override changes where
a dependency resolves. It never changes what a store declares.
A remote store is third-party content. StoreGraph marks a member
remote transitively, and it refuses a local path that a remote member
declares.
Selector::parse reads a key:value filter, and matches_all applies
a set of them to one item.
For stable file names beside human-readable ones:
slugify("Fix the Widget!")returns"fix-the-widget"generate_prefix(&existing)returns a 4-character id such as"ab12", avoiding the ids already inexistingextract_prefix("ab12-fix-the-widget")returnsSome(("ab12", "fix-the-widget"))has_prefix("ab12-fix-the-widget")returnstrue
The mcp feature carries the pieces the three tools share when they
serve a store to a Model Context Protocol client: the surface
configuration, the access mode, the document URI form, and the content
digest.
mdstore = { version = "0.5", package = "mdstore-core", features = ["mcp"] }The feature is off by default, so a consumer that builds only a CLI does not pull in the server stack.
- tisket — issue tracker. Markdown issues with YAML frontmatter, in the repository
- zettel — zettelkasten notes for a repository
- almanac — agent skill index, over pluggable sources
- gaff — context-lifecycle handler for coding agents
- missouri — end-to-end tests as directed graphs of filesystem states
tisket, zettel, and almanac store their documents with mdstore.
MIT.