A Source's Journey Through PathMX

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.

1. The durable unit is a Source

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.

2. Identity comes from the repository boundary

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 S be the set of loaded Sources and B(s) the Blocks parsed from Source s. The total indexed Block population is

|B|=∑s∈S|B(s)|.

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.

3. Parsing creates evidence, not a second authority

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 h headings, l links, f fences, and b Blocks, a useful structural summary is the tuple I(s)=(h,l,f,b). That tuple is not a complete performance model, but a sudden change from I(s)=(8,12,2,4) to I(s)=(800,1200,200,400) 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.

4. Indexing is incremental by Source

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.

5. The graph is rebuilt from explicit relationships

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 V is the Source set and E the resolved-link set, the repository graph is G=(V,E). A full rebuild should be near O(|V|+|E|); 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.

6. The complete lifecycle

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.

Miss Hit Markdown Source on disk Environment reads UTF-8 bytes Parse frontmatter and Markdown structure Normalize Source id and type Extract authored Blocks Index headings Index outgoing links Index fenced content Replace Source-owned index facts Resolve targets Rebuild Source graph Authorize and resolve page request Select page Source Compiled page cached? Create compile context Run precompile hooks Compile Markdown to HTML Run postcompile hooks Run page hooks Render Mermaid to inline SVG Protect TeX from Markdown parsing Highlight code tokens Render protected TeX as MathML Collect scripts and styles Hash document assets Assemble semantic document Store compiled page cache Attach current theme asset Serve HTML response Runtime enhances navigation and Actions Live refresh observes relevant changes Optional search projection Optional static export Optional diagnostics and provenance

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.

7. Compilation is an ordered conversation

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 p runs over contexts Cp and takes time t(p,c) for context c. Its total contribution is

Tp=∑c∈Cpt(p,c).

A performance trace should therefore report both Tp and maxc∈Cp⁡t(p,c). A high total with a low maximum suggests repeated overhead; a high maximum points toward one pathological Source, Block, or diagram.

8. Mermaid is build-time content

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 d be the number of diagrams, ni the nodes in diagram i, and ei its edges. A useful observation vector is D=(d,∑ini,∑iei,maxi⁡ni). Diagram count alone cannot describe a dense layout problem.

9. Code highlighting should scale with code, not prose

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.

10. Math is stashed before Markdown compilation

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 h=H/(H+M) for cache hits H and misses M. If a hit costs Th and a miss costs Tm, average request work is

E[T]=hTh+(1−h)Tm.

This page also exercises a matrix, A=[1101], a set R={s∈S|s is reachable from the Root}, and a bounded sum ∑i=1d(ni+ei). Each expression has architectural meaning while forcing the renderer to handle more than the smallest $x$ case.

11. Assets are page facts before they are URLs

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.

12. Cache entries are replaceable projections

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 P be the page set, C⊆P the pages compiled after an edit, and A⊆P the pages whose artifacts could change. Correct invalidation requires A⊆C; efficient invalidation tries to minimize |C∖A|. 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.

13. Requests still cross an authority boundary

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 allow=f(principal,operation,source,currentFacts). The word “current” is important: a cached page compiled before a roster or membership change must not bypass the new access decision.

14. A request-to-live-refresh sequence

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.

write Markdown Source M2 changed source /guides/journey.path index, rebuild affected graph facts compile page and immutable assets publish cache generation G2 GET /guides/journey resolve Principal and current access read authorized page from G2 HTML plus asset references 200 document from Source M2 subscribe to live page events write corrected Source M3 invalidate affected page publish cache generation G3 page changed; reconcile GET current document authorized document from M3 Author or CI Source repository PathMX engine Authorized Host Browser Runtime

Quoted messages are intentional. They protect punctuation-rich PathMX prose that has historically exposed differences between Mermaid parsers.

15. Runtime enhancement preserves document semantics

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.

16. Actions are trusted mutations, not arbitrary scripts

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.

17. Live state has an explicit lifecycle

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.”

authorized navigation document and assets accepted request or document rejected live subscription established relevant Source changed replacement document accepted refresh failed; current view retained retry or reconnect explicit reload full navigation required retry page closed page closed Unloaded Loading Current Failed Watching Refreshing Stale

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.

18. A small performance model

For one cold Source compilation, separate setup, transformation, layout, assembly, and retention:

Tcold=Tload+Tindex+Tcompile+Tassets+Tlayout.

For a warm watched edit, loaded modules and bundled document assets can be reused, giving approximately Twarm=Treindex+Taffected+Treplace. Neither equation guarantees that warm work is cheap; it merely identifies which terms should disappear.

Peak memory should also be decomposed. Let R0 be resident memory before the feature activates, Rf the feature's retained runtime, Ri temporary input and layout state, and Ro retained output. Then

Rpeak≈R0+Rf+Ri+Ro.

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.

19. Measure complete operations

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.

20. Portability lives in capability seams

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.

21. Source changes should have narrow consequences

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 Δs, define deps(Δs) as the proven dependent pages. The ideal work set is W={s}∪deps(Δs). A system that recompiles all |P| pages for every edit has cost O(|P|) even when |W|=1; a system that compiles only s 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.

22. Themes remain document-level capabilities

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.

23. Failure should preserve authored meaning

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.

24. A request diagnostic in TypeScript

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.

25. A portable benchmark record

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.

26. Review the output structurally and visually

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.

27. Interpret cold and warm results separately

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 x1,…,xn, 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.

28. Define a heavy reference case, not an infinite worst case

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.

29. Operational checklist

When a PathMX page seems slow or stale:

  1. Identify the exact Source, route, Host, runtime, and corpus snapshot.
  2. Confirm whether the request compiled, read cache, or served a static export.
  3. Inspect compile.page and per-plugin totals plus maximum labeled contexts.
  4. Separate cold renderer activation from warm diagram layout.
  5. Compare RSS before activation, after activation, and after many pages.
  6. Verify the rendered HTML contains expected code, MathML, and SVG artifacts.
  7. Measure direct origin, local gateway, and public HTTPS independently.
  8. Confirm current authorization and generated asset responses.
  9. Inspect the browser's visible page and console before calling it healthy.
  10. 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.

30. The architectural through-line

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:

durable Source⟶derived, replaceable views.

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.