The release that turns a personal Obsidian script into a tool someone else can run. Every path is resolved from configuration, every check is a library function the CLI merely formats, and the claim the whole format rests on, that a quote is verbatim, is finally verified by a program instead of asserted by an agent.
The command surface
- Ten subcommands —
init,ingest,index,lint,verify,status,graph,ledger,extract,skills. The CLI stays deliberately thin: each one resolves a config, calls a single library function, and formats the result. Logic that lives in a CLI cannot be tested or reused, so none does. - Every command is importable —
from ugraph import config, indexes, lint, status, verify. Formatting, exit codes and argument parsing belong to the CLI and nowhere else. - Shared selectors —
--newest N,--since 7d|2w|3m|1y|YYYY-MM-DDand--channelare defined in one place so they cannot come to mean different things in different commands.
Verification — the headline change
ugraph verifychecks every quote against the immutable transcript. Averbatim_quotemust be a literal substring; atimestampmust be a marker that really exists, with the quote inside that marker's paragraph. Extraction agents reported verifying this. Nothing ever checked it.- Two passes, deliberately asymmetric. Machine output is held to a strict standard because a program cut it from the transcript. Human-edited prose is checked timidly, because quotes get re-wrapped, elided, and surrounded by paraphrase, and the checker stays silent rather than accusing a page wrongly. A checker that cries wolf gets muted, and this one has to survive being trusted.
- Nothing is ever repaired. Machine captions carry their own disfluencies and mistranscriptions, preserved inside quotes on purpose.
- A weak model became safe to use. Because a paraphrase fails a substring test rather than a trust check, a small local model can do Phase A at the cost of retries: the generation–verification loop applied to the tool that documents it.
Portability
- The KB root is configuration, not an import. Resolution order:
--kb,$UGRAPH_KB, thekbkey in the nearestugraph.toml, that file's own directory, then the working directory if it looks like a KB. The previous implementation hardcoded one vault path; that single fact was all that stood between a personal script and a tool. store.pyreplaces the borrowedvault_iomodule. Markdown with YAML frontmatter, JSON state, plain-text logs. No database, because the premise of the format is that an agent needs nothing but a filesystem.- Page identity is a POSIX-separated relative path on every platform, since that string appears in links and has to be the same on every machine.
initrefuses to scaffold into a vault root. Pointing it at an Obsidian vault used to scatterconcepts/,entities/,raw/andsources/among real folders, write anindex.mdthat could clobber a note, drop the config file outside the vault, and then report every personal note as malformed. It now refuses and names where to put it instead.- Relative
candidatespaths resolve against the KB root, like every other path inugraph.toml. They previously resolved against the working directory, so the setting appeared to do nothing depending on where you ran the command from.
The format, made executable
- Seven page types with enforced contracts —
concept,entity,source,raw-transcript,moc,overview, andnotefor human-facing pages held to almost no contract on purpose. - Eight typed relationship headings with loose reciprocity: if A links to B under a typed heading, B must link back under some typed heading.
sourcesis the exception: a transcript never links forward to its readers. - Wikilinks are a schema error inside the strict tree. A
[[wikilink]]only resolves inside the tool that invented it, and the format's premise is that a plain filesystem is enough to follow an edge. _mocs/is scaffolded. It is part of the strict tree and policed by the linter, so leaving it uncreated meant the first person to add one discovered the rule by tripping over it.- A closed taxonomy, falling back to the packaged vocabulary so a bare
initproduces something that lints.
Gates you can wire into CI
lint,verifyandindex --checkexit non-zero and stay quiet on success, so they drop into a pre-commit hook or a CI job unchanged.lint --warningspromotes warnings to failure;lint --report PATHwrites markdown.- Index rebuilds are byte-identical. Same contents in, same bytes out, which is what makes
--checkmeaningful: if a rebuild would change any file, the working tree is lying about what the KB contains. - Unrecognized groups land under "Other" rather than being silently dropped.
- Parse failures are never silently skipped in a structural check. Dropping unreadable pages would produce a confident answer about a KB that was never fully read, so
strictis the default and the caller has to opt out deliberately.
Ingest
- Any-input capture — clipboard, paste, pipe, file, or URL lands as an immutable
raw/capture plus a stubsources/page. No model involved; ingest only transports content, it never invents it. - Incremental and resumable. State is checkpointed after every written source rather than once at the end, so large runs are safe to interrupt and safe to re-run in whatever batch size you like.
--newest Non batch ingest for bounded pulls without paying for a full metadata scan first.--retry-failedreconsiders sources previously recorded as unfetchable.
Extraction — opt-in, never in the core
- Three backends.
claude-codehands the work to the agent through the installed skill;ollamaruns locally over plain HTTP with no new dependency;apiuses Anthropic or OpenAI through the optional extra. - Phase A only. Reading one transcript and emitting candidates with verbatim quotes and timestamps is mechanical work. Phase B, deciding which candidates merge into which canonical page, is deliberately excluded: it needs every candidate in view at once, it is where the value of the format lives, and there is no mechanical check for getting it wrong.
- One prompt contract shared by the API backend and the agent skill, so the two cannot drift.
- The deterministic core calls no model at all. Everything here is an extra.
Knowing where you stand
ugraph ledger— six lifecycle stages (discovered → pulled → extracted → synthesized → linked → verified) derived from the files on every run, never stored in frontmatter. A stored stage would be a second source of truth and would drift the first time somebody edited a page by hand.- Transitions are appended to a log, because derivation can say where a source is but not when it got there or what failed on the way.
--stuck DAYSsurfaces material pulled but unprocessed. - The ledger keys on the universal slug, not on anything video-shaped, so a future blog, newsletter or podcast adapter appears in every report the day it lands, with nothing added to the ledger.
ugraph statussplits cleanly intocollect()(every number, no formatting) andrender()(one terminal view), with--thinfor concepts still resting on a single source.- Undated pages sort last and are dropped by
--since. A naive descending sort put undated pages first, so--newest 3would confidently return pages whose date nobody knows as the three most recent. A page that cannot prove it falls inside the window is not in the window.
Seeing the graph
- Six export formats —
json,graphml,dot,canvasfor Obsidian,obsidian-groups, andd3for a standalone interactive HTML page with force layout, zoom and drag. --concepts-onlyand--no-provenance, because sources outnumber concepts and swamp a visual.- The export stays derived and disposable. Markdown is the source of truth; regenerating a 64 KB export costs milliseconds, so there is no reason to make it authoritative.
First run
- An interactive
ugraph initthat gets a stranger from nothing to a working knowledge base without reading anything. Every question has a sensible default and every answer is also a flag, so scripts and CI never see a prompt. ugraph skills installwrites the agent instructions into.claude/skills, so the agent doing Phase A learns the format from the same contract the linter enforces.
Known gaps in this version
- More automatic fetch adapters (RSS, podcast, web page) are still on the backlog; today the primary path is any text you can land via the CLI into the same
raw/andsources/pair — UG-05, UG-06. templates/andskills/resolve from a source checkout when the packaged copy is absent — UG-01.- No published package and no tagged release yet — UG-02.
- No automated test suite over the three gates — UG-03.