# myform — the universal docs-format translator > **Spec of record** for the myform universal-translator pivot. > Confirmed with the operator 2026-07-20; supersedes the scope implied by the package's current name. > Provenance: `~/my/self/dredge/memy/26-07-13_typst-first-fleet-migration-prompt.md`. ## `1.` Vision `myst-mdformat` becomes **`myform`**: one absolute, typed document AST in the middle, format frontends and backends around it. :::{admonition} The one-sentence contract :class: important `myform.convert(text, src, dst)` parses `src`-format text into a lossless document tree and renders that tree as idiomatic `dst`-format text, degrading unrepresentable constructs *visibly and intelligently* — never silently. ::: Formatting is the identity special case: `convert(text, f, f)` **is** the formatter, and for MyST/GFM it must remain byte-identical to what the mdformat plugin pipeline produces today. That fixed-point property is the safety rail for the whole pivot: the existing corpus and CommonMark-compliancy suites keep their full force. ### `1.1.` Launch matrix (confirmed 2026-07-20 — superseded by shipped state) ```{note} This table, and the "Read 2, write 4" decision in §2 and the reading non-goals in §1, record the plan as confirmed on 2026-07-20. All four readers shipped in v1.0.0, including the Typst reader this table defers. `README.md` carries the current matrix; [`universal-graph-and-sites.md`](universal-graph-and-sites.md) carries the horizon beyond it. The plan is preserved here as written rather than rewritten, because a spec of record is evidence of what was decided. ``` | Format | Read | Write | Engine | | :------------ | :------------- | :-------- | :------------------------------------------------- | | MyST Markdown | ✅ launch | ✅ launch | markdown-it pipeline (existing `Parser`) | | GFM | ✅ launch | ✅ launch | markdown-it pipeline (mdformat-gfm) | | Sphinx rST | 🔜 fast-follow | ✅ launch | docutils (read); native writer (write) | | Typst | ⏳ deferred | ✅ launch | native writer; see §8 and the Typst-ecosystem memo | ### `1.2.` Non-goals at launch - Typst *reading* (no viable Python parse path today — see the [Typst-ecosystem memo](typst-ecosystem-memo.md)). - Pandoc-style long-tail formats (LaTeX, docx, ...). The architecture must not preclude them; the launch must not wait for them. - Semantic rewriting beyond degradation (the LM seam is for lossy-boundary judgment calls only). ## `2.` Design decisions (operator-confirmed, 2026-07-20) 1. **Pivot in place.** This repo becomes `myform` (PyPI name verified free). Git history, test corpus, and CI carry over. The mdformat plugin survives as one facade inside the new package. 2. **Read 2, write 4.** Readers: MyST, GFM. Writers: MyST, GFM, rST, Typst. 3. **LM assists are opt-in and degrade-only.** Off by default; zero network calls when disabled; deterministic fallback always defined; routed through the local gateway (`localhost:4000`); disk-cached by node hash. 4. **Lossy policy: annotate + report.** Nearest-equivalent replacement that carries attendant info (captions, labels, headers); a provenance comment where the target supports comments; a per-file conversion report. Zero silent losses. 5. **basis, lightest cut.** `my-basis` becomes a dependency for `RegexStore` (wikiparse's idiom) and light utilities — but import cost is measured and budgeted (§10); heavy leaves stay lazy. ## `3.` Architecture The pipeline generalizes from `MD -> AST -> MD` to: ``` readers writers MyST ──┐ ┌── MyST (via token bridge + mdformat) GFM ──┤→ markdown-it tokens → ┐ ├── GFM (via token bridge + mdformat) rST ──┘ (docutils, follow-up) ├→ Doc ──┤── rST (direct AST walk) ┘ │ └── Typst (direct AST walk) │ degradation engine + assist seam (runs on Doc, per-writer capability table) ``` ### `3.1.` Package layout ``` myform/ ├── __init__.py # public API: convert(), format(), Doc; PEP 562-lazy like basis ├── model/ │ ├── nodes.py # typed node model (myst-spec/mdast-aligned) │ ├── bridge.py # tokens ⇄ Doc (bijective for the markdown pipeline) │ └── walk.py # visitors, transforms, node addressing ├── readers/ │ ├── base.py # Reader protocol + registry │ ├── myst.py # markdown-it + Parser rules → Doc │ └── gfm.py # markdown-it + gfm rules → Doc ├── writers/ │ ├── base.py # Writer protocol, Capability table, registry │ ├── myst.py # Doc → tokens → mdformat render (fixed point) │ ├── gfm.py # same engine, GFM capability table │ ├── rst.py # direct AST walk │ └── typst.py # direct AST walk ├── degrade/ │ ├── engine.py # capability-driven lowering pass over Doc │ ├── lowerings.py # declarative lowering rules (node kind × target) │ └── report.py # ConversionReport (+ stderr summary, JSON sidecar) ├── assist/ │ ├── base.py # Assist protocol; NullAssist (default) │ ├── gateway.py # localhost:4000 client (lazy import, stdlib HTTP) │ └── cache.py # disk cache keyed by (node hash, target, model) ├── config/ │ ├── options.py # MyformConfig (generalizes MystOptions; per-format tags) │ └── extensions.py # .py extension loading (register readers/writers/lowerings) ├── markdown/ # the existing engine, relocated intact: │ ├── Parser.py, Renderer.py, Spacer.py, Postprocessor.py, │ ├── Sembr.py, Ignore.py, constants.py ├── plugin.py # mdformat entry point facade (unchanged behavior) ├── cli.py # `myform` CLI (convert + format verbs) ├── options.py # MystOptions (mdformat-facing surface, kept) └── utils.py # NO_ESC, parse_match, ... (RegexStore migration lands here) ``` `myst_mdformat` (the old import path) is **not** preserved — the package has no external consumers yet; a clean break now is the whole point of pivoting before launch (dredge note). The mdformat entry-point name `myst` is preserved, so `mdformat --extensions myst` and `mdformat.text(..., extensions={'myst'})` behave identically. ### `3.2.` The token bridge (the load-bearing novelty) MyST/GFM writing does **not** reimplement CommonMark rendering. Instead `model/bridge.py` maintains a faithful mapping between markdown-it token streams (including our `myst_*` token types) and the typed `Doc` tree: Reader path : `markdown-it tokens → Doc` — every token becomes a typed node; unknown/plugin tokens are captured as `UnknownNode` with their full token payload (absoluteness guarantee). Writer path (markdown targets) : `Doc → tokens → mdformat.MDRenderer` with this package's `RENDERERS`/`POSTPROCESSORS` — so spacing (`Spacer`), sembr, ignore-comments, and every existing MyST opinion apply unchanged. The bridge must satisfy `to_tokens(from_tokens(T)) == T` for every token stream the launch readers produce; this is property-tested over the whole corpus and the CommonMark spec suite. This is what makes AC3 (fixed point == today's output) hold *by construction* rather than by re-implementation. rST and Typst writers walk `Doc` directly; they never see tokens. ## `4.` The core AST (`myform.model`) Node vocabulary aligns with [myst-spec](https://mystmd.org/spec) / mdast so we inherit a documented, ecosystem-tested taxonomy instead of inventing one: - **Flow**: `Root`, `Paragraph`, `Heading`, `Blockquote`, `List`/`ListItem`, `Code`, `ThematicBreak`, `Table`/`TableRow`/`TableCell`, `Definition*`/`Footnote*`, `Math`, `Directive`, `Target`, `Comment`, `BlockBreak`, `FrontMatter`, `Ignored` (verbatim span), `Unknown`. - **Phrasing**: `Text`, `Emphasis`, `Strong`, `Delete`, `InlineCode`, `InlineMath`, `Link`, `Image`, `Break`, `FootnoteReference`, `Role`, `InlineUnknown`. Every node is a pydantic model with: `type` : The myst-spec node name (discriminator). `children` : Typed child list (flow or phrasing, enforced). `span` : Source position when the reader had one (line/col start–end); `None` on synthesized nodes. `info` : Format-agnostic core fields (e.g. `Heading.depth`, `Code.lang`, `Directive.name/args/options`, `Role.name`). `raw` : The escape hatch — the originating token/payload preserved verbatim, so nothing the reader saw is ever unrecoverable (`Unknown` nodes are *only* `raw`). Design constraints: - **No behavior in nodes.** Rendering/lowering logic lives in writers and the degradation engine; nodes are data (N-02: policy/mechanism separation). - **Attendant-info accessors.** `Directive.caption`, `Directive.label`, `Table.header`, `Image.alt` etc. are derived properties used by the degradation engine's carriage rules — one definition, every writer benefits. - **Hashing.** Every node has a stable content hash (used by the assist cache and the report). ## `5.` Writers and capability tables Each writer declares a **capability table**: a mapping `node kind → Capability` where ```python class Capability(Enum): NATIVE = auto() # renders idiomatically, no loss APPROX = auto() # rendered via a lowering rule; report as 'approximated' CARRY = auto() # cannot render; attendant info is carried into a replacement; report as 'replaced' DROP = auto() # cannot render; dropped with a report entry (never silently) ``` The degradation engine (§6) consults the table *before* the writer runs, lowering the `Doc` to a tree the writer renders 100% natively. Writers therefore stay simple — they never contain "if this can't be rendered" branches (N-06: the constraint lives in one gate). Illustrative capability decisions (full tables live beside each writer and are corpus-tested): | Node | MyST | GFM | rST | Typst | | :------------------- | :----- | :------------------------------------------ | :------------------ | :-------------------------------- | | Admonition directive | NATIVE | APPROX → `> [!NOTE]` alert | NATIVE (`.. note::`) | APPROX → prelude callout / quote | | Role | NATIVE | CARRY → content + report | NATIVE | APPROX (`ref`/`cite` map; else CARRY) | | Target | NATIVE | CARRY → `` when HTML allowed, else DROP | NATIVE (`.. _t:`) | NATIVE (`