Weaveback is a bidirectional literate programming toolchain. Write your source code inside AsciiDoc (or any text file), expand macros, assemble named chunks, and let the tool write the real files — then trace any generated line back to its origin, or propagate edits back to the source.
weaveback source.adoc --gen srcUnder the hood two passes run in sequence:
-
weaveback-macro — expands
%macro(…)calls -
weaveback-tangle — extracts
<[@file …]>chunks and writes them to disk
Both passes run in-process; no intermediate files or subprocesses.
Traditional literate programming tools are one-way: you generate code, but edits must go back manually. Weaveback makes the pipeline bidirectional:
-
Trace any generated line back to its source (
weaveback where) -
Propagate edits from generated files back to the document (
apply-back) -
Integrate with editors and agents via MCP
The source document is the single source of truth — but never a dead end.
A single document can define data once and fan it out to multiple files in multiple languages simultaneously. Add one entry; every target stays in sync.
The example below defines a set of plugin events in AsciiDoc and generates a C header, a Rust module, and a SQL seed file from the same macro call. Built-in case conversion macros derive every name from two base words — no redundant parameters, nothing to get out of sync:
%def(event, id, ns, name, description, %{
// <[c enum arms]>=
EVT_%to_screaming_case(%(ns))_%to_screaming_case(%(name)) = %(id),
// @
// <[rust enum arms]>=
%to_pascal_case(%(ns))%to_pascal_case(%(name)),
// @
// <[rust display arms]>=
PluginEvent::%to_pascal_case(%(ns))%to_pascal_case(%(name)) => write!(f, "%(ns).%(name)"),
// @
// <[sql rows]>=
INSERT INTO audit_event_types (id, name, description)
VALUES (%(id), '%(ns).%(name)', '%(description)')
ON CONFLICT (name) DO NOTHING;
// @
// <[c cleanup array]>=
cleanup_%(ns)_%(name),
// @
%})
%event(0, plugin, load, "Plugin loaded into host")
%event(1, plugin, unload, "Plugin removed from host")
%event(2, config, change, "Configuration key changed")Chunk slots compose recursively and carry indentation from the skeleton, so the generated files look exactly as you would write them by hand:
/* include/events.h — generated */
typedef enum {
EVT_PLUGIN_LOAD = 0,
EVT_PLUGIN_UNLOAD = 1,
EVT_CONFIG_CHANGE = 2,
EVT__COUNT
} PluginEvent;
/* @reversed slot — cleanup runs in reverse registration order */
static const void (*EVT_CLEANUP[])(void) = {
cleanup_config_change,
cleanup_plugin_unload,
cleanup_plugin_load,
};// src/events.rs — generated
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginEvent {
PluginLoad,
PluginUnload,
ConfigChange,
}
impl fmt::Display for PluginEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PluginEvent::PluginLoad => write!(f, "plugin.load"),
PluginEvent::PluginUnload => write!(f, "plugin.unload"),
PluginEvent::ConfigChange => write!(f, "config.change"),
}
}
}-- db/seed_events.sql — generated
INSERT INTO audit_event_types (id, name, description)
VALUES (0, 'plugin.load', 'Plugin loaded into host')
ON CONFLICT (name) DO NOTHING;
INSERT INTO audit_event_types (id, name, description)
VALUES (1, 'plugin.unload', 'Plugin removed from host')
ON CONFLICT (name) DO NOTHING;
INSERT INTO audit_event_types (id, name, description)
VALUES (2, 'config.change', 'Configuration key changed')
ON CONFLICT (name) DO NOTHING;The @reversed slot attribute on EVT_CLEANUP expands the accumulated
chunk in reverse order — destructors fire in the right sequence with no
manual reordering. See the full example including the
justfile that wires weaveback, meson, and asciidoctor together.
When built-in macros — case conversion, %if, %equal — aren’t enough,
the macro language offers two scripted extensions for arbitrary calculation
and string manipulation:
-
%rhaidef— define a macro whose body is a Rhai script, a small expression language compiled into the binary. No external runtime. -
%pydef— define a macro whose body runs in a sandboxed Python interpreter (monty), also compiled in. No Python installation required.
%rhaidef(offset, base, n, %{(parse_int(base) + parse_int(n)).to_string()%})
%offset(100, 3)The tradeoff: generated lines that pass through a scripted macro are opaque
to weaveback trace — the expander cannot map them back to a source location.
Use them where traceability of the expansion itself is not critical.
Arch Linux: paru -S weaveback-bin
Nix: nix profile install github:giannifer7/weaveback
Quick (musl, any Linux):
curl -sL https://github.com/giannifer7/weaveback/releases/latest/download/weaveback-musl \
-o /usr/local/bin/weaveback && chmod +x /usr/local/bin/weaveback→ Full installation guide (all platforms, binaries, musl vs glibc)
cd examples/events
weaveback events.adoc --gen .This expands macros in events.adoc, extracts all @file chunks, and
writes them under the current directory.
To see what the macro expander produced before tangle runs:
weaveback events.adoc --gen . --dump-expanded 2>expanded.txt| Document | Contents |
|---|---|
All flags for |
|
|
|
|
|
|
|
All platforms, package managers, pre-built binaries |
weaveback mcp starts an MCP server over stdin/stdout, exposing three tools
so AI agents and IDE extensions can work directly with the literate source:
-
weaveback_trace— find the exact source line behind any generated line -
weaveback_apply_fix— oracle-verified edit (no rebuild needed to confirm) -
weaveback_apply_back— propagate bulk gen/ edits back to source
The recommended agent workflow is: trace → read context → apply_fix → check prose → build.
apply_fix re-runs the macro expander as an oracle and writes the file only
if the expected output is produced — catching wrong edits before the build.
See Source tracing for the full workflow, gotchas, and
.mcp.json configuration.