PathMX starts with a deliberately ordinary object: a Markdown Source that a person can read, edit, diff, and keep. Everything else—the indexed graph, compiled HTML, theme assets, live page, and cached representation—is derived from that Source. This guide follows one Source through the complete pipeline and uses the pipeline itself as a substantial rendering exercise.
The governing constraint is simple: authored truth should stay legible even when every renderer is absent. A Source may gain diagrams, executable Actions, math, highlighted examples, and live behavior, but none of those projections should become the only copy of the learner's or author's work.
Use the lifecycle map for the whole route, the performance model for the cost vocabulary, and the operational checklist when diagnosing a slow or stale page.
A Source is UTF-8 Markdown plus optional frontmatter. Its path gives it a stable place in the repository; its parsed identity gives the rest of PathMX a canonical way to refer to it. Headings, links, fenced code, and authored Block boundaries remain visible in the file instead of being hidden in an opaque database row.
This small example carries metadata, prose, a relative link, inline math, and a code fence without requiring a proprietary editor:
---
title: Conservation of energy
access:
visibility: public
---
# Conservation of energy
For an isolated system, $E_{before}=E_{after}$.
Read the [worked example](./worked-example.path), then explain which
quantities moved between kinetic, gravitational, and thermal stores.
```ts
const total = kinetic + gravitational + thermal
```
The outer fence is intentionally highlighted Markdown. The nested TypeScript is content inside that example rather than another live fence, demonstrating that syntax can be discussed without accidentally activating it.
The repository discovers Markdown files and normalizes filenames into Source
identifiers. /notes/energy.path.md becomes /notes/energy.path; route logic
can later decide which Source types are pages without changing the underlying
identity. Extensions such as path, log, issue, demo, and components
are semantic vocabulary, not an invitation to invent separate storage systems.
Let be the set of loaded Sources and the Blocks parsed from Source . The total indexed Block population is
This matters operationally because a plugin can run once per Source, once per Block, or once per page. A page with one hundred Blocks may therefore be more expensive than a similarly sized page with one Block even when both contain the same number of characters.
Loading extracts frontmatter, headings, links, fences, and Blocks. The parsed records are indexes over the file: they make queries cheap, but they do not replace the Markdown as the durable authority. If a cache disappears, PathMX can reconstruct it from Sources. If the Source disappears, a cache is not a safe substitute for authorship or provenance.
For a Source with headings, links, fences, and Blocks, a useful structural summary is the tuple . That tuple is not a complete performance model, but a sudden change from to is a strong signal that per-node plugin work deserves inspection.
Parsing also establishes the boundary at which real bytes matter. A file that claims to be an image but contains another format should be classified by its bytes at the media workflow boundary; a Markdown Source should not need to carry that normalization logic in every downstream plugin.
Indexing turns parsed evidence into lookup structures: Source identity, headings, outgoing links, backlinks, actor profiles, access facts, and plugin specific indexes. The desirable unit of invalidation is the changed Source and the proven consumers of facts derived from it.
The following sketch shows the shape of an index plugin. It derives a compact heading catalog and replaces exactly one Source's old contribution when that Source changes:
import type { Plugin } from "@pathmx/core"
type HeadingFact = {
source: string
id: string
depth: number
text: string
}
export function HeadingCatalogPlugin(): Plugin {
const bySource = new Map<string, HeadingFact[]>()
return {
index(source) {
bySource.set(
source.id,
source.headings.map((heading) => ({
source: source.id,
id: heading.id,
depth: heading.depth,
text: heading.text,
})),
)
},
remove(source) {
bySource.delete(source.id)
},
}
}
The important abstraction is ownership. The plugin owns its derived map; the repository owns Sources; neither reaches into the other's representation to mutate it indirectly.
Links become graph edges after their targets are resolved. The graph can answer questions such as “what refers here?”, “what is reachable from this Root?”, and “which pages depend on this component?” without repeatedly scanning Markdown. A missing target stays observable as a diagnostic rather than being silently rewritten into a successful edge.
If is the Source set and the resolved-link set, the repository graph is . A full rebuild should be near ; an edit that changes only the prose of one Source should not require every Source to be reparsed. The graph is a projection of authored relationships, so its persistence is an optimization rather than a new source of truth.
The graph also keeps navigation honest. Breadcrumbs, tables of contents, and related-page lists are consumers of graph facts. They should not infer a parallel hierarchy from whatever DOM happens to be visible in the browser.
The main pipeline is small enough to draw, but large enough to expose where cost and authority live. Solid arrows are required work for a build; the side branches are derived capabilities that can be delayed when their facts are not needed for the current page.
No single arrow grants permission to skip the authority boundary. A cached page may avoid recompilation, but an HTTP request still resolves credentials and access before protected output is returned.
Plugins participate through narrow hooks. precompile sees Markdown and may
stash syntax that the Markdown compiler would otherwise consume.
postcompile sees HTML and can replace safe placeholders. page sees the
whole compiled page and can contribute document assets. Ordering is part of
the contract because one plugin's output can become another plugin's input.
For this page, Mermaid runs before the default stack so diagrams become slots before the code highlighter sees fenced content. Math protects TeX before Markdown parsing and replaces its placeholders afterward. Code highlighting operates on ordinary code blocks but never double-highlights SVG created by the Mermaid renderer.
Suppose hook runs over contexts and takes time for context . Its total contribution is
A performance trace should therefore report both and . A high total with a low maximum suggests repeated overhead; a high maximum points toward one pathological Source, Block, or diagram.
Build-time Mermaid keeps the served document simple: the browser receives inline SVG rather than a diagram parser, layout engine, and client-side render queue. That improves determinism and avoids layout shifts, but the build process must pay parser and layout cost. The current beautiful renderer uses ELK for graph layout, so its first graph also activates a material resident working set.
This fixture includes three different diagram families on purpose. The large flowchart stresses graph layout, the sequence diagram protects the quoted message syntax PathMX authors use, and the state diagram exercises a separate parser and rendering path. Successful compilation must produce three SVG figures, not three highlighted Mermaid code blocks.
Let be the number of diagrams, the nodes in diagram , and its edges. A useful observation vector is . Diagram count alone cannot describe a dense layout problem.
The highlighter searches compiled code blocks, identifies languages from their classes, and tokenizes only languages it knows. Prose and Mermaid SVG should take fast paths. Unknown languages remain escaped, readable code rather than becoming build failures.
The environment adapter owns bundling of the small copy-button client. A Bun environment can use Bun's native bundler while another runtime supplies the same capability through its own adapter:
export function createEnvironment(runtime) {
return {
async readText(path) {
return runtime.files.readUtf8(path)
},
async bundle(entry) {
const result = await runtime.bundle({
entrypoints: [entry],
minify: true,
target: "browser",
})
return result.outputs
},
hash(text) {
return runtime.crypto.sha256(text).slice(0, 8)
},
}
}
The portable engine asks for “bundle this client entry”; it does not import a Bun-only API. That seam is what lets the same core architecture target Bun, Node, Workers, and future hosts without pretending their filesystem and module systems are identical.
Dollar-delimited TeX overlaps with ordinary Markdown punctuation, inline code, and currency prose. The math plugin first masks inline code and fenced regions, then stashes recognized display and inline expressions. After Markdown has produced HTML, the plugin replaces numbered placeholders with MathML.
For example, the cache hit ratio is for cache hits and misses . If a hit costs and a miss costs , average request work is
This page also exercises a matrix,
, a set
, and a bounded sum
. Each expression has architectural meaning while
forcing the renderer to handle more than the smallest $x$ case.
Plugins contribute named stylesheets and scripts to the compile context. Document assembly deduplicates those assets, hashes their content, and exposes immutable URLs. A page without math should not receive the math stylesheet; a page without code should not receive the copy-button client.
The distinction between an asset fact and its served URL is useful. Plugins
can say “this page needs math.css” without predicting the final content hash
or route. The document layer can then combine identical assets across many
pages and keep caching policy centralized.
A compact manifest might look like this:
{
"source": "/guides/source-journey.path",
"assets": [
{ "type": "stylesheet", "name": "theme.css", "immutable": true },
{ "type": "stylesheet", "name": "math.css", "immutable": true },
{ "type": "stylesheet", "name": "code-highlight.css", "immutable": true },
{ "type": "script", "name": "code-highlight.js", "module": true }
],
"document": {
"view": "main[data-pmx-view]",
"live": true
}
}
The manifest is illustrative rather than a second configuration format. The real API remains typed objects passed between plugins and the engine.
Compiled pages are cached by canonical Source identity. A clean build compiles every page Source; a watched edit should invalidate only pages whose output can actually change. If a shared component or global style changes, dependency facts may fan that invalidation out to multiple pages. If ordinary prose changes, global invalidation is evidence of an overly broad dependency.
Let be the page set, the pages compiled after an edit, and the pages whose artifacts could change. Correct invalidation requires ; efficient invalidation tries to minimize . Compiling fewer pages is not a success if a genuinely affected page stays stale.
Cache replacement should be atomic from the reader's perspective. A request observes the old complete page or the new complete page, not half-updated HTML paired with a new theme hash.
Compilation answers “what would this Source render as?” Request handling answers “may this Principal receive it now?” Those are different questions. Public Sources, private Homes, internal actor records, shared Sources, and attachments all use the same resolved request credentials but can apply different access facts.
The server should avoid revealing whether a protected Source exists to an unauthorized caller. Exact access decisions can use the Actor directory and Source access index internally while the external response remains non-enumerating.
An authorization decision can be modeled as . The word “current” is important: a cached page compiled before a roster or membership change must not bypass the new access decision.
The sequence below makes ownership explicit. The browser owns navigation intent, the Host owns credentials and authorization, the engine owns compiled page projections, and the repository owns current Sources. A live event is a hint to reconcile; it is not itself the durable content.
Quoted messages are intentional. They protect punctuation-rich PathMX prose that has historically exposed differences between Mermaid parsers.
The served HTML is complete enough for ordinary navigation and forms without JavaScript. The Runtime enhances eligible same-origin navigation, Action submission, theme switching, and live refresh. It does not become the only authority for routes or mutations.
When enhanced navigation succeeds, the Runtime replaces one explicit view, reconciles document metadata and assets, restores focus and scroll behavior, and rebinds live observation. When it cannot prove that the response is a compatible PathMX document, normal browser navigation is the recovery path.
The desired hierarchy is progressive: semantic document first, enhancement second. This is why performance measurements should include HTML bytes and first contentful paint rather than reporting only client bundle execution.
An Action receives an authenticated context, validates input, and proposes a mutation inside the Host's authority boundary. The browser may request the Action, but it cannot grant itself access or write directly into another Actor's Home.
This Python example is intentionally a conceptual adapter rather than a PathMX production API. It demonstrates the data boundary a non-JavaScript host would need to preserve:
from dataclasses import dataclass
@dataclass(frozen=True)
class MutationRequest:
operation_id: str
actor_id: str
source_id: str
expected_version: str
markdown: str
def admit(request: MutationRequest, authority, repository):
current = repository.version(request.source_id)
if current != request.expected_version:
return {"type": "conflict", "current": current}
if not authority.may_write(request.actor_id, request.source_id):
return {"type": "denied"}
return {"type": "accepted", "operation": request.operation_id}
The durable invariants are portable even if transaction and filesystem APIs differ by host: exact Actor identity, current authorization, optimistic conflict detection, deterministic operation identity, and an auditable Source transition.
A page moves through a small number of states. Keeping them explicit prevents “there is a socket” from being mistaken for “the learner sees current work.”
Stale is not the same as Failed. A transient live-refresh error can leave a
usable document on screen while honestly reporting that it may no longer be
current. An uncertain Action result is different again: the mutation may have
completed, so blind resubmission can be unsafe.
For one cold Source compilation, separate setup, transformation, layout, assembly, and retention:
For a warm watched edit, loaded modules and bundled document assets can be reused, giving approximately . Neither equation guarantees that warm work is cheap; it merely identifies which terms should disappear.
Peak memory should also be decomposed. Let be resident memory before the feature activates, the feature's retained runtime, temporary input and layout state, and retained output. Then
If output is one megabyte but RSS rises by two hundred megabytes, optimizing SVG whitespace cannot explain or repair the dominant term. Conversely, a renderer that uses little memory but emits repeated megabytes of inline CSS may still be a poor cache and network tradeoff.
A useful benchmark names its lifecycle. “Render time” might mean parsing one warm diagram, importing a renderer and producing SVG, compiling a Source, building all pages, or navigating a browser to first contentful paint. Those numbers answer different questions.
For this heavy reference Source, report at least:
- environment and runtime version;
- Source bytes, lines, Blocks, code fences, math expressions, diagram families, nodes, and edges;
- cold setup and compile time in a fresh process;
- warm median and p95 compile time after feature activation;
- HTML and document-asset bytes;
- RSS before compilation, RSS after the cold compile, and OS maximum RSS;
- plugin warnings and counts of actually rendered artifacts.
The benchmark should fail if diagrams silently fall back to code, math remains as raw dollar syntax, or code highlighting stops tokenizing. Faster incorrect output is not a performance improvement.
The core needs clocks, hashing, file discovery, file reads, asset bundling, and possibly durable stores. It should ask an environment for those capabilities instead of importing one runtime's globals throughout repository and engine code.
The shell below illustrates a portable operator contract: inputs and outputs are explicit, the Source root is not inferred from a home directory, and a diagnostic run can be isolated from production state.
set -euo pipefail
source_root="${1:?pass a Source root}"
output_root="${2:?pass an output directory}"
bun run pathmx/core/cli/index.ts build \
--root "$source_root" \
--out "$output_root" \
--print-url
test -f "$output_root/index.html"
A Node, Worker, or Vercel adapter may expose different mechanisms, but it can preserve the same observable operation: load one Source graph, compile the selected pages, and return explicit artifacts or diagnostics.
An edit can affect parsing, graph facts, compiled HTML, shared components, styles, or authorization metadata. Invalidation should be derived from those facts. Filename suffixes and broad directory membership are convenient hints, but they are not sufficient evidence that every page depends on a Source.
For a change , define as the proven dependent pages. The ideal work set is . A system that recompiles all pages for every edit has cost even when ; a system that compiles only when a shared component changed is fast but wrong.
Observability makes that tradeoff reviewable. A watched edit should log which Source changed, which dependency rule selected each affected page, how many pages compiled, when the new page became visible, and whether optional search or export projections are still catching up.
Theme tokens let authored content adapt without baking one palette into every Source. Mermaid SVG should use the same background, foreground, surface, border, muted, and accent variables as prose and code. MathML should inherit the surrounding text color. Highlighting may provide token colors while still respecting contrast in both light and dark modes.
This CSS example shows the intended seam rather than a separate design system:
:root {
--pmx-color-bg: oklch(0.985 0.006 255);
--pmx-color-fg: oklch(0.25 0.025 255);
--pmx-color-surface: oklch(0.96 0.012 255);
--pmx-color-border: oklch(0.82 0.025 255);
--pmx-color-accent: oklch(0.58 0.16 245);
--pmx-font-body: Inter, ui-sans-serif, system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--pmx-color-bg: oklch(0.19 0.02 255);
--pmx-color-fg: oklch(0.92 0.015 255);
--pmx-color-surface: oklch(0.24 0.025 255);
--pmx-color-border: oklch(0.38 0.035 255);
}
}
.pmx-mermaid svg,
[data-pathmx-math] {
max-inline-size: 100%;
}
Theme verification must inspect real rendered artifacts. A CSS variable in Source code is only intent until the browser shows that SVG text, lines, surfaces, and MathML remain legible in both schemes.
If Mermaid parsing fails, the original fenced diagram remains readable and a Source-local warning identifies the failure. If TeX is unsupported, its text remains visible in an error wrapper. If a highlighter does not know a language, the escaped code remains available. These are graceful degradations because authored evidence survives.
The same principle applies at larger boundaries. A static export failure must not corrupt the repository. A live refresh failure must not erase the current view. A denied write must not partially mutate related Sources. A cache miss may compile from current Sources, but it must not fabricate content from stale or unrelated artifacts.
Operational status should be equally precise: provider success is not mailbox delivery, HTTP 200 is not browser acceptance, and a generated asset is not a live route until its document actually references and serves it.
The diagnostic below keeps phases separate so a slow public route does not collapse origin, gateway, and browser behavior into one number:
type Timing = {
label: string
status: number
headersMs: number
bodyMs: number
bytes: number
}
async function measure(label: string, url: string): Promise<Timing> {
const started = performance.now()
const response = await fetch(url, { redirect: "manual" })
const headers = performance.now()
const body = await response.arrayBuffer()
const finished = performance.now()
return {
label,
status: response.status,
headersMs: headers - started,
bodyMs: finished - headers,
bytes: body.byteLength,
}
}
const rows = await Promise.all([
measure("origin", "http://127.0.0.1:3000/guides/journey"),
measure("gateway", "http://127.0.0.1:8787/guides/journey"),
measure("public", "https://example.test/guides/journey"),
])
console.table(rows)
A multi-second 200 is degraded, not healthy. Separating time to headers from body transfer also distinguishes server work from unexpectedly large output.
Machine-readable output makes before/after comparisons less dependent on log formatting. This example records provenance and keeps cold, warm, and memory observations distinct:
{
"benchmark": "heavy-source-v1",
"runtime": { "name": "bun", "version": "1.3.14" },
"machine": { "class": "same-mbp", "arch": "arm64" },
"fixture": {
"source": "/examples/pathmx-source-journey.demo",
"codeBlocks": 9,
"mathExpressions": 58,
"mermaidDiagrams": 3
},
"cold": { "samples": 3, "metric": "compile.page" },
"warm": { "samples": 5, "summary": ["median", "p95"] },
"memory": { "before": "rss", "after": "rss", "external": "max-rss" },
"correctness": {
"requireHighlightedCode": true,
"requireMathML": true,
"requireInlineSvg": true,
"allowRendererWarnings": false
}
}
Numbers are intentionally absent from the authored example. Baselines belong in the performance log beside the exact commit, runtime, and command that produced them; the Source fixture defines work, not a forever claim about one machine.
Structural assertions catch missing work cheaply: count three Mermaid figures, nine ordinary code wrappers, the expected MathML wrappers, required assets, and zero renderer warnings. They also verify that the large flowchart labels and quoted sequence message survive compilation.
Visual review catches what counts cannot: clipped labels, edge crossings, illegible dark-mode colors, horizontally overflowing equations, copy buttons covering code, and a table of contents that becomes unwieldy. A performance fixture is still a document; if it looks absurd, it is a poor representative of the product experience.
The two review modes complement one another. Screenshots alone cannot prove that every equation rendered, and string counts cannot prove that a 40-node diagram is readable.
Cold activation answers what happens when a small build or CLI process meets its first expensive Source. It includes dynamic module import, renderer initialization, initial asset bundling, and the first parse/layout operation. Warm compilation answers what happens during subsequent edits while those modules and caches remain resident.
Neither is universally more important. A long-lived development Host cares about warm edit visibility and retained memory. A serverless build or one-page export may care more about cold latency. A full static build cares about throughput across many distinct Sources and total retained output.
For repeated warm samples , report the median to resist one scheduler interruption and p95 to expose tail behavior. Do not silently discard the first sample and then describe the result as cold.
There is no finite worst Source unless PathMX imposes limits on Source bytes, Block count, code tokens, TeX complexity, and diagram nodes and edges. This fixture instead defines one reviewable reference case that is large enough to exercise realistic expensive paths while remaining understandable to a human.
The bounds are part of the evidence. If the fixture later grows from three diagrams to thirty or from thirty kilobytes to three megabytes, its baseline must not be compared to the old one as though the work were unchanged.
Separate adversarial limit tests can cover malformed or intentionally enormous input. Their job is bounded failure and resource safety. This fixture's job is representative heavy rendering with correct, attractive output.
When a PathMX page seems slow or stale:
- Identify the exact Source, route, Host, runtime, and corpus snapshot.
- Confirm whether the request compiled, read cache, or served a static export.
- Inspect
compile.pageand per-plugin totals plus maximum labeled contexts. - Separate cold renderer activation from warm diagram layout.
- Compare RSS before activation, after activation, and after many pages.
- Verify the rendered HTML contains expected code, MathML, and SVG artifacts.
- Measure direct origin, local gateway, and public HTTPS independently.
- Confirm current authorization and generated asset responses.
- Inspect the browser's visible page and console before calling it healthy.
- Record the command, runtime, machine, result, and remaining uncertainty.
This sequence keeps diagnosis source-first. It narrows the failing boundary before restarting shared infrastructure or changing architecture.
PathMX can remain small because each layer has one job. The repository owns durable Sources. Parsing and plugins derive facts. The graph relates them. The compiler creates replaceable documents. The cache accelerates reads. The Host owns credentials and authority. The Runtime enhances an already meaningful page. Optional search and exports consume the same current graph at the edge.
The whole lifecycle is summarized by one invariant:
Performance work should protect that direction. Faster derivation is valuable; making a cache or browser session the only surviving copy of authorship is not. The best optimization removes repeated derived work while leaving Source ownership, provenance, and authorization easier—not harder—to explain.