@egildo/provenance
v0.8.0
Published
Knows, for one rendered document, every source involved, how the sources depend on each other, and where each came from.
Readme
A rendered document is made of many files. By the time a page appears in a browser, it has read an include tree, a configuration cascade, stylesheets, fonts, images, and whatever plugins pulled in along the way. Two questions follow every render: what did it actually load, and which files must change on disk when someone edits what they see.
Provenance is one structure that answers both. For a single rendered document, it knows every source involved, how those sources depend on each other, where each rendered thing came from, and how to stage and write changes back — without ever touching a filesystem itself.
import { openSession } from "@egildo/provenance";
import { createNodeHost } from "@egildo/provenance/node";
import { css } from "@egildo/provenance/css";
import { html } from "@egildo/provenance/html";
const session = await openSession({
host: createNodeHost(),
entry: "/path/to/index.html",
handlers: [html, css],
});
for (const source of session.sources()) {
console.log(source.id, source.state, source.location);
}
session.onChange(change => console.log(change));Why
Most tools that need this answer it twice, with bookkeeping that can't talk to itself. BelType — the project Provenance was extracted from — kept a list of files read, a separate list of URLs loaded, two near-identical file watchers, and one hand-rolled rule mapping URLs back to files. Each piece answered one question. None of them could answer the other, because they shared no coordinates.
Provenance gives every fact — an edge, a stamp, an edit — one coordinate system: a source, a version, and a byte range. Anything it knows can be joined against anything else it knows.
What it does
- Tracks sources of any kind. Markdown, HTML, CSS, configuration, images, fonts — and whatever a handler teaches it. Binary files are leaves: addressable, never parsed.
- Records dependencies twice. Declared edges come from parsing; observed edges come from what a runtime actually fetched. The two are kept apart on purpose — the gap between them is the product, not noise to smooth over.
- Addresses rendered output. Any rendered node can point back to the exact source, version, and byte range that produced it.
- Stages edits. Changes are held against a base version, previewed in memory, and written to disk together. What gets written is exactly what the preview showed.
It runs in any JavaScript realm — Node, a browser tab, a worker — because it performs no I/O of
its own. Everything outside the call (reading, writing, watching, path arithmetic) is asked of a
host the embedder supplies. A Node host ships as its own entry point; nothing else in the
library imports from node:, and a test enforces that rather than trusting convention.
How it decides things
Provenance is opinionated on purpose. Nine design principles govern it, each one stated as something it refuses to do:
| Principle | Forbids |
|---|---|
| One address space | A fact with no address. A second coordinate system past the handler boundary. |
| Handlers declare; Provenance decides | A handler that reads, resolves, caches, watches, or writes. |
| Derived facts are pure functions of versions | Any invalidate() call, anywhere. A cache hit that skips re-registering its probes. |
| Declared and observed stay apart | An observation that overwrites a declared edge. A disagreement smoothed away. |
| Source is the stored reality; renders are projections | Writing Provenance's own bookkeeping into a source file. |
| Nothing touches disk before commit | Any write outside commit. A staged edit expressed against another staged edit's result. |
| Edits are splices | A printer or serializer that regenerates a source from a tree. |
| Languages are plugins; nesting is delegation | Language knowledge in the core. A handler parsing another language's region itself. |
| Supply, not meaning | A node: import outside the Node host. A host that returns a verdict instead of a fact. |
The full reasoning — with the incidents each rule was cut from — is in the design principles.
Status
The kernel is built, and editing's first slice. Observation is not.
A session finds every source reachable from an entry document and its roots, records each declared dependency against the exact bytes that asked for it, keeps broken references visible until they heal, and reports what changed as files change. See the kernel spec for exactly what that covers, and the changelog for what shipped in each release.
Since 0.5.0 it also writes back: a tool stages an edit to a value, sees a preview, and commits it; the edit lands in exactly the bytes of that value, every other byte untouched, and survives the file changing on disk in the meantime (the design, the editing spec). Writing is granted, never ambient. Since 0.6.0 it also inserts and deletes entries (insert-delete), and an edit staged on a file that a colleague is mid-way through saving waits for it to parse again. Since 0.7.0 a commit lands every file or none (atomic-commit), and the staged edits can be saved and restored across a reload (saveable-index). Format plug-ins come from outside — Cascata's YAML and JSON writers are the first.
What's next: stamps (outputs that carry their source address, so an edit on a rendered page finds its way back), Markdown units and a CSS writer, then observation — see the vision's "What arrives first".
Install
npm install @egildo/provenanceIts tests prove write-back end to end with Cascata, a development dependency, @egildo/cascata.
Requires Node 22 or later (22.18+ to run the tests from source, for type stripping).
Entry points
| Import | What it gives you |
|---|---|
| @egildo/provenance | openSession and the core types |
| @egildo/provenance/html | An HTML handler, on parse5 |
| @egildo/provenance/css | A CSS handler, on PostCSS |
| @egildo/provenance/node | The Node host |
| @egildo/provenance/memory | An in-memory host, for any realm — tests, browsers, workers |
Every export is listed in the public API contract.
Who it's for
- BelType, first — replacing its file list, URL list, two watchers, and eventually its editing server's write path.
- Cascata, by shape and not by import — a read pass's host satisfies Cascata's
Hostinterface structurally, so Cascata's configuration files become session roots without Cascata ever importing Provenance. - The CSS tools, which build on Provenance's addresses and its edit model.
- Any tool that renders documents from files and needs to know what it read, what the runtime loaded, and how to write back.
What it deliberately stays out of — rendering, transforming, reading meaning, serving, history — is in the vision's "What it is not", each exclusion argued for.
Documentation
- Vision — what Provenance is for, who it serves, what it refuses to be.
- Design principles — how it decides things, each rule with what it forbids.
- Glossary — every christened term, and which meaning of "provenance" is ours.
- Design synthesis — the day-zero design the vision was drawn from.
- Kernel spec and read-passes spec — what's built, precisely.
Development
npm test # type-checks and runs the tests
npm run build # writes dist/