cedit — continuous editing of vendored Markdown
Keep local adaptations of vendored Markdown alive across upstream
updates: a persistent block-level overlay, re-applied by 3-way structural
merge on the document's AST. Vendored a skill whose commands assume bash
but your environment runs zsh? Rewrite the fences once — every later
sync re-applies your rewrite over whatever upstream changed, and tells
you precisely (per block, with all three versions) when upstream touched
the same thing you did.
Documentation
The user-facing docs are published at sdlctools.github.io/cedit, versioned alongside releases — a reader on an older cedit gets the docs that match it. The rest live in the repository, where the tooling that reads them expects them.
| Document | What's in it |
|---|---|
| User guide | How to drive it — a five-minute tour, a per-flag reference for all five subcommands and for the md parser views, the conflict lifecycle worked end to end, the .cedit/ layout, a cookbook and a troubleshooting table |
| SPEC.md | The design — the merge matrix, the normative sync algorithm, the state format, the reuse rules, and what is phase 1 vs. phase 2 vs. never |
| AGENTS.md | Changing cedit itself — build and test commands, the architecture in one table, and the five invariants a change must not violate. CLAUDE.md exists only to pull this in, so every AI assistant reads the same file |
| ARCHITECTURE.md | The code, and how to change it — every function, dataclass field and constant, the end-to-end call graph from cli.main down to the splice, where each invariant is actually enforced, and a Changing cedit section: which changes move consumers' stored hashes, and what to touch to add a subcommand, a block kind or a state field |
| .claude/rules/release-pipeline.md | How this repo ships — the two tag shapes, the dev-build / cut / release flow end to end, who owns the version at each step, the five workflow invariants, and a failure-mode table |
| cedit-canonicalization-reference.md | Canonicalization reference — every Markdown element and how cedit md canonicalize transforms it, known caveats, and quick test commands |
Using cedit? You want the user guide. The last three are for working on it.
The pinned parser and the Merkle-hash diff engine live — frozen — in
cedit/mdcore/.
Every hash cedit records is a function of them, so they change only through
the drift check described in
.claude/rules/hash-stability.md.
Install
pipx install cedit # or: pip install cedit
cedit --help
Python 3.10 or newer — every version cedit claims is a version CI runs the suite on, which is the whole point of the claim. 3.10 and 3.11 are the Ubuntu 22.04 and Debian 12 system interpreters, and cedit is a developer tool that lands on whatever Python a machine already has.
That installs the cedit command and the importable package with the
pinned parsing stack as real dependencies. The docs write cedit <subcommand> throughout; python3 -m cedit <subcommand> is the same entry
point with the same arguments, and is what you want when cedit lives in a
virtualenv you'd rather not activate.
Install cedit into an environment of its own — that's what pipx above
buys you; a dedicated virtualenv does the same. mdcore/utils.make_parser
appends every installed mdformat parser extension, so the set of mdformat
plugins present in the environment is part of the parser identity. Dropping
cedit into a shared environment that already carries other mdformat plugins
can move the hashes in your .cedit/ state even though cedit's own pins are
honoured — and moved hashes read as conflicts against blocks nobody touched.
Working on cedit itself
Developing cedit rather than using it? Work from a source checkout:
python3 -m venv venv
venv/bin/pip install -r requirements.txt # the parsing stack is pinned EXACTLY — see the file
venv/bin/pip install -e . # optional: only to run cedit from another repo
venv/bin/python3 -m pytest # 105 tests, no network
Quickstart
Run from the root of the repository holding your vendored copies — a
different repo than this one. State lives in .cedit/ (commit it — the
base snapshots are the merge's memory).
# 1. start tracking (vendors the file if it doesn't exist yet)
cedit snapshot skills/SKILL.md --from vendor/skills/SKILL.md
# 2. adapt the file in place — e.g. rewrite bash fences for zsh — then:
cedit diff
# [edit opaque fence] #c564262de9cbba0f:0 sim=0.98
# ctx : 1. Discovery and healthcheck
# base : bash "${CLAUDE_PLUGIN_ROOT}/.../ensure_local_env.sh" || exit 1
# local: zsh "${CLAUDE_PLUGIN_ROOT}/.../ensure_local_env.sh" || exit 1
# 3. upstream evolved — merge it in (your edits re-apply, even across moves
# and reflows; upstream changes to blocks you didn't touch flow in)
cedit sync --from vendor
# skills/SKILL.md: 1 edit(s) reapplied, 1 block(s) updated from upstream, 1 conflict(s)
# [CONFLICT opaque fence] #c564262de9cbba0f:0
# base : bash ".../ensure_local_env.sh" || exit 1
# upstream: bash ".../ensure_local_env.sh" --quiet || exit 1
# local : zsh ".../ensure_local_env.sh" || exit 1 (kept in the working file)
# 4. a conflict means upstream changed the very block you adapted — decide:
cedit resolve skills/SKILL.md c564262de9cbba0f --show # all three versions
cedit resolve skills/SKILL.md c564262de9cbba0f --take local # keep the adaptation
cedit resolve skills/SKILL.md c564262de9cbba0f --take upstream # take upstream's text
cedit status
# skills/SKILL.md: 2 local edit(s), 0 unresolved conflict(s); base 92b023942934d656 ...
Exit codes: 0 clean, 1 unresolved conflicts, 2 errors. A document
with open conflicts refuses to sync again until they're resolved, and the
working file always keeps your text on a conflict — resolution is
explicit, never a silent clobber.
Everything above in depth — every flag, every output line, the conflict
lifecycle end to end, the .cedit/ layout and a troubleshooting table — is
in the user guide.
Looking at the parser directly
Those five subcommands are stateful. cedit md is a group of stateless
verbs — a file (or stdin) in, stdout out, no .cedit/ touched — for seeing
what the parser actually does to a document:
cedit md canonicalize SKILL.md # the mdformat round-trip .cedit/base/ stores
cedit md canonicalize --check SKILL.md # exit 1 if it isn't already canonical
cedit md blocks SKILL.md # the blocks the merge keys on, with hashes
cedit md ast --hashes SKILL.md # the parse tree, every node's Merkle hash
cedit md json SKILL.md | cedit md from-json # md -> tokens -> md, losslessly
md blocks is the one to reach for when a conflict key is a mystery: it
prints the same <hash>:<occurrence> keys that cedit status and cedit resolve speak.
What it will not do (yet)
- Local structural changes — inserting, deleting or moving whole blocks — are detected and rejected with a per-block report (phase 2 in the spec). Phase 1 merges replacements: prose rewrites, fence rewrites, table-cell tweaks, front-matter edits.
- Fetching upstream.
--fromtakes a directory (mirroring your doc paths) or a file; git submodules, subtrees or curl are your transport. - Link reference definitions are inlined when used, and unused ones are dropped with a warning on stderr. See the user guide, Limits, stated plainly.
Layout
| Path | |
|---|---|
cedit/__main__.py |
the python3 -m cedit entry point |
cedit/cli.py |
the five subcommands: snapshot / diff / sync / status / resolve |
cedit/mdcli.py |
the md group: stateless parser views — canonicalize / ast / json / from-json / blocks |
cedit/merge3.py |
the 3-way merge matrix + overlay derivation |
cedit/align.py |
block-sequence alignment (LCS over Merkle hashes, moves, fuzzy) |
cedit/blocks.py |
block extraction, splicing, render-and-verify |
cedit/mathguard.py |
the $...$ math guard: carry the math through canonicalisation byte-exact, warn on stderr about what it cannot |
cedit/rowguard.py |
the table-row guard: carry what a body row holds past the header's last column through canonicalisation byte-exact, warn on stderr about what it cannot |
cedit/state.py |
.cedit/ — base snapshots, manifest (+ conflicts), overlay |
cedit/store.py |
atomic writes: temp file + rename(2), so a crash never leaves half-written state |
cedit/mdcore/ |
frozen: the pinned parser + tree_diff — every recorded hash is a function of these |
tests/ |
merge matrix + end-to-end CLI lifecycle + packaging metadata + the parser drift check |
docs/ |
the four published documents: the user guide, the spec, the architecture map and the canonicalization reference |
website/ |
the Docusaurus site that publishes docs/ — self-contained, and excluded from the sdist and the wheel |
pyproject.toml |
packaging metadata: the exact runtime pins, the cedit console script, explicit package discovery |
.github/workflows/tests.yml |
the suite on 3.10 – 3.14, installed from requirements.txt |
.github/workflows/docs.yml |
builds website/ and deploys it to GitHub Pages, path-filtered to docs/** and website/** |
Status
Alpha (Development Status :: 3 - Alpha). The merge is phase 1: it
re-applies replacements — prose, fences, table cells, front matter — and
rejects local structural changes (inserting, deleting or moving whole
blocks) with a per-block report rather than guessing. Structural local edits
are phase 2 in
SPEC.md. The CLI
surface, the exit codes and the .cedit/ state format are what phase 2 will
build on, but nothing here is promised stable before 1.0 — pin the version if
that matters to you.
License
MIT — see LICENSE.
Metadata
Release files for cedit 0.3.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cedit-0.3.6.tar.gz | 73.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cedit-0.3.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 121.9 kB
Release files / cedit-0.3.6.tar.gz
| Download URL | cedit-0.3.6.tar.gz |
|---|---|
| Size | 73.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8f74d50d4ca9bd8a2642bf3a1b8886cc06d565acb252b8fcf3a80a3d5a2578bf
|
|
BLAKE2b-256 checksum How to use checksums |
09e451d303787a8ebb95016c9ef20c7245c55f646f5462e8365ab01bf0b4832b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 14, 2026.
Transparency logRelease files / cedit-0.3.6-py3-none-any.whl
| Download URL | cedit-0.3.6-py3-none-any.whl |
|---|---|
| Size | 48.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90d91888d7b9b018117cdb8ab4070863ee4bc81c9b649c57bda4257e94e5f285
|
|
BLAKE2b-256 checksum How to use checksums |
708d4794e49b0a24365a44b7bf1529113cec10bf2f21b4be8095d69cb3695fa2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 14, 2026.
Transparency log