Principles
These are not style preferences. Each one closes a failure that has already happened in a project like this one, and each one has a price that is paid on purpose.
One source, two faces
Every Markdown story has a typed JSON twin: the same content, validated by the format's schema, convertible in both directions and tested in both directions. Neither is the original.
Why. A human wants to write prose and read a diff. A machine — an agent, a CMS, a script — wants a structure it can validate. Pick one and you lose the other; keep a converter you do not test and the two drift within a month.
The price. Every block, directive and attribute has to exist twice, in the parser and in the schema, and a round-trip test runs over every story in the repository. A format cannot be half-added.
One type system
Shared types — the story, the scene, attributes, configuration, diagnostics, translations — are defined once, in the core, derived from the zod schemas. A format package never redefines one.
Why. The second definition is never a copy for long. It gains a field, loses a constraint, and then two parts of the same program disagree about what a scene is.
The price. The core holds more than feels natural, and moving a type is a change that ripples.
One stack
No dependency that duplicates one already present. No second way of doing something that already has a way.
Why. A monorepo with two bundlers, two test runners or two date libraries spends its maintenance budget on the seams between them.
The price. Sometimes the slightly worse tool wins because it is already there.
Content is not trusted
Raw HTML in a Markdown story is sanitized by default. A directive cannot raise its own privileges — it cannot lower the diagram security level, inject props React treats specially, or forge the mounting of a component. MDX is the exception and is treated as code, never sanitized, because that is what it is.
Why. A story often comes from somewhere else: a CMS, a pull request, an agent reading a PDF. The author of a story is not always the owner of the site that renders it.
The price. A deliberate escape hatch has to be configured, and an .mdx file has to be reviewed
like code.
Nothing is published quietly
A release is manual and approved by a human. An agent never publishes. Every bundled asset and every non-permissive dependency is listed before it is merged, with its licence.
Why. This repository imports code written by someone else, and it embeds fonts and generated images. Attribution that is added later is attribution that is wrong in between.
Accessibility is a diagnostic, not a lint rule
A diagram without an accessible name, a slide too dense to read, an image without a description: these produce diagnostics that travel with the story and are reported by whatever builds it — the command line and a site build.
Why. A warning nobody sees is a warning that does not exist. This rule found a real defect the day it was enforced: a diagram in one of the example decks had never had an accessible title, and site builds had been swallowing the message for weeks.
The site framework is reached through its plugins
The formats reach the site framework through its plugin interface, @recit/site-stories, rather
than by changing its core.
Why. A plugin is easier to test and to move than a change scattered through the framework, and the published packages stay independent of a framework that is never published.
The price. Some things are done the long way, through a plugin, when a three-line change would do.