Carbon Docs
docs is a built, opinionated documentation site — Fumadocs + Next.js (React 19), light-only,
warm-paper aesthetic. It is no longer scaffolded; it exists and ships content. This skill is how to add
or change docs in that system, in its house style, grounded in real Carbon code.
Announce at start: "Using the carbon-docs skill — authoring docs for {topic}."
First: ask which kind of page this is
Not all doc pages are equal — a settings toggle does not get the treatment a production flow gets, and
the single biggest quality failure so far has been writing every page in the long narrative register.
Before any research or writing, ask the user to pick the page type (AskUserQuestion, one question,
your recommended type first — infer it from the topic). Skip the question only when the user already
named the type in their request, or you're editing an existing page without changing its shape (keep
its current type). Several pages in one request → one question covering them.
| Type |
Reader's question |
Shape |
Budget |
| Flow / tour (Guide) |
"walk me through it" |
Narrative chapter in a flow: sequenced, illustrated, second-person storytelling, prior stages set up |
length serves the story |
| System / entity (Reference) |
"how does it work — fields, statuses, rules?" |
1–2 lead sentences → <StatusFlow entity=…> lifecycle → rule/gotcha Callouts → <FieldTable> → <Cards> onward |
~800–1,500 words |
| Settings / how-to (Reference) |
"how do I turn it on / use it?" |
1–2 lead sentences → <Steps> → one gotcha Callout → where-it-lives |
~300–600 words; steps, not story |
| Concept / overview (Reference) |
"what is this area?" |
Short intro + <Cards> linking to the owning pages |
~150–300 words |
A page can mix types by section (an entity page that ends with a short settings section) — pick the
dominant type for the page, then hold each section to its own shape. Troubleshooting/FAQ content never
gets a reader-visible section: it goes in <AgentContext> (see Reference components).
No page ends cold. Every page closes pointing onward: entity and concept pages with a small
<Cards> Related row, settings pages with where-it-lives links, guide chapters get "read next" for
free. A page that just stops is an IA bug, not a style choice.
The house voice below scales with this choice: full narrative register belongs to Guides; entity pages
dial it down to scannable-with-gotchas; settings/how-to pages drop it entirely. On a settings page the
reader wants the steps, not an essay — two-factor auth is a 500-word how-to, not an 1,800-word
meditation on passwords.
The single biggest mistake is writing plausible ERP-generic prose. Carbon's behavior is specific and
often counterintuitive (WIP is a GL balance not a table; a payment is a posted entity applied via
settlements; overhead IS absorbed into WIP per production event; fixed-asset disposal is
scrapping-only). Every claim is grounded in source. See the
prime directive below — it overrides everything.
What Carbon is: a manufacturing system — ERP (the office) + MES (the floor), one platform over
one data model. Academy is not a third product pillar: it just hosts training video lessons, not a
SaaS app a customer buys. Never frame Carbon as "ERP, MES, and training/Academy"; site, meta, and landing
copy stay ERP + MES. (The academy app can appear in an architecture/monorepo listing — fine; describe
it as video-lesson hosting, not a pillar.)
The prime directive — ground everything in source
The holy source of truth is the actual source code + the LATEST database migrations
(packages/database/supabase/migrations/, newest by timestamp) — NOT ERP/CMMS general knowledge, NOT
.claude/rules/ alone (it is often stale).
- Verify before you write. Every entity, status enum value, and transition named in docs must exist
in real code. Confirm exact strings (
"To Ship and Invoice", "Fully Depreciated"), the actions that
drive them (service fns, routes, edge functions in packages/database/supabase/functions/), and what
posts/gates (e.g. companySettings.accountingEnabled).
- Read the newest migration, not the first. Timestamps order them; a 2026 refactor may have rebuilt a
subsystem the cache still describes the old way.
- Document only real, ACTIVE features. Omit placeholders / inactive / not-yet-shipped things (e.g.
integration registry entries with
active: false like QuickBooks/Sage). Don't surface them.
- Deliberately undocumented: the Implementation Hub (
/x/get-started, packages/onboarding
content) stays out of the docs by the user's decision — don't document it, and don't link to it.
- When code and cache disagree, code wins — and note the drift.
- Method: one research subagent per feature → a structured fact sheet with
file:line refs
(contract in Authoring workflow step 1) → then write from it. This is how every flow in the Guide was
built. Don't skip it for anything non-trivial.
Three surfaces (know which you're touching)
| Surface |
Path / route |
What it is |
Authored? |
| Guides |
content/guides/*.mdx → /guides |
Editorial narrative tours, grouped into flows. Second-person, opinionated, illustrated. The journey + the "why". |
Hand-written MDX |
| Reference |
content/docs/**/*.mdx → /docs |
One page per entity/concept. Scannable: tables, cards, field rows. The "what". |
Hand-written MDX |
| API reference |
app/api-reference/[module]/[resource] |
PostgREST endpoint docs, generated at build from the swagger. |
Generated — do NOT hand-edit. Edit scripts/generate-api-docs.mjs or the swagger schema. |
Interlink across surfaces and flows: a Guide links into Reference for detail; Reference links back to the
Guide for the story. (Guide chapters were given 12 cross-flow links — interlinking is expected, not optional.)
Authoring workflow (every change)
- Research (grounded) → a fact sheet. Dispatch a research subagent per feature. It must return a
structured fact sheet, not prose: entities/tables/columns; exact status strings and which entity
owns each; transitions with the action that drives them (service fn / route / edge function) as
file:line; gates and settings (companySettings.*, plan features); the real UI labels for every
where-to-click; the gotchas a generic description would get wrong; and an explicit "could not verify"
list — a gap on that list is more research work, never a writing improvisation. The writer then works
ONLY from the fact sheet + the page-type template, and writing stays in the main loop on the main
model — no model downgrades for prose; the fact sheet is the handoff, not a license for a weaker
writer.
- Pick the surface + placement (the page type was chosen up front). Guide flow + frontmatter, or
Reference folder +
meta.json + REFERENCE_GROUPS — run the placement checklist below before
creating any new page.
- Write in the house voice with the surface's real components (below). Lead with a concrete example.
Decide media placement yourself as you write — don't wait to be asked. Drop a
<Screenshot> slot
wherever the reader needs to see the actual UI, with a label naming the real screen + state (the fact
sheet's UI labels are the source); use <Figure> only when a registry key genuinely fits. Typical
density by page type: settings/how-to 1 (the screen with the switch/steps), entity 1–2 (the list or
detail view, or the lifecycle UI), guide chapters one per major beat, concept stubs none. Boy Scout
rule: editing an existing Reference page that has no <Screenshot>? Add the one slot its type calls
for while you're there — the Reference surface shipped with almost none, and it goes on the media
manifest like any new slot. Reuse existing slots on a rewrite; never drop or re-label one that a real
capture may already exist for. Slot labels double as the image's alt text once a capture lands, so
write them as a description of what's shown (screen + state), never as a caption or a filename.
- Verify — against the user's running dev server, read-only. They usually have
pnpm --filter docs dev
up. Never pkill/restart it, run next build, or rm .next under it — verify by fetching pages:cd docs
pnpm exec fumadocs-mdx # regen .source if a content/frontmatter change didn't HMR
curl -sS -X GET http://localhost:3002/docs/<slug> | grep -o "your new heading" # confirm it rendered
A green pnpm --filter docs build (✓ Generating static pages N/N) is the gold standard — but only in a
clean checkout / when no dev server is running. tsc is noisy (a React 18/19 @types skew that
next.config ignores) — grep your own files out of the errors rather than expecting zero.
Guides — the flow architecture
Frontmatter (source.config.ts schema):
---
title: From quote to order
description: An RFQ becomes a quote; an accepted quote becomes a sales order.
label: "(I)" # display marker, roman numeral, per-flow
index: 0 # order WITHIN the flow (0,1,2…)
flow: quote-to-cash # flow id (omit → defaults to "make-to-order")
flowName: Quote to cash # flow tab label
flowIndex: 1 # order of the flow in the subnav (0 = first)
---
- Chapters sort by
(flowIndex, index). The subnav is a flow switcher; the **sidebar + mobile selector
- "read next" are scoped to the active flow**. The original 5 chapters (order/build/plan/floor/ship) carry
no flow fields and fold into
make-to-order (flowIndex 0) via defaults.
- Add a chapter to a flow: same
flow/flowName/flowIndex, next index + label.
- Add a flow: new
flowIndex + flowName; its chapters start at index: 0, label: "(I)".
- All chapter bodies render server-side on every page (the reader cross-fades, no route nav), so links to
/guides/<slug> resolve. No code fences in guides (prose + components only).
Components (components/editorial/mdx.tsx):
<Figure illustration="flow-overview" caption="…" /> — SVG from the registry. illustration MUST be a
real key from components/editorial/illustrations.tsx (else it silently renders nothing). Valid keys:
flow-overview, order-split, bom-tree, demand-forecast, planning-engine, shopfloor-loop, eight-d, traceability-graph, method-types, kit-vs-subassembly, reorder-policy, outside-processing, mes-station, issue-workflow, schedule-board, get-method, conversion-factor, opportunity-thread, cash-cycle, rfq-fanout, receive-bill-axes, wip-inflow, wip-to-cogs, depreciation-curve, asset-exit. For anything
without a fitting key, use <Screenshot> instead — don't invent keys.
<Screenshot label="Sales order dashboard" caption="…" ratio="wide|tall|square" /> — a standing slot for
a real Carbon capture. Use it where the reader needs to see the actual UI (dashboard, a specific
form/field, status, board, where-to-click) — not as decoration. Label the real, current screen + state
precisely (verify it exists) so a real screenshot can drop straight in and the docs stay synced to the live
Carbon UI. Default ratio="wide".
<Callout tone="neutral|blue|green|amber" badge="WHY BATCH" title="…">body</Callout> — the workhorse.
Use it to carry the Carbon-specific truth a generic description gets wrong. Tones: blue = explanatory
"why", green = good-to-know/outcome, amber = caution/contrast, neutral = definitional.
<Divider /> — closes a chapter before its wrap-up line.
<Term>make to order</Term> — inline glossary term: dotted-underline; click/tap opens a popover with a
grounded one-line definition + an optional "Learn more" link. Same component on both surfaces; definitions
live in @carbon/glossary (packages/glossary/src/terms.ts), rendered by
docs/components/editorial/glossary.tsx. See "Interlinking & the glossary" below.
Each ## heading becomes a sidebar rail entry — so structure chapters as 3–5 ## sections.
Reference — entity pages
- Frontmatter:
title + description (+ plan for a paid-feature gate).
- Placement checklist — run it BEFORE creating any page. (1) Does an existing page already own this
topic? Grep
content/docs for the feature name; if a page covers it, extend that page with a
section instead of creating a parallel one — a bucket page that re-documents features owned elsewhere
is scattering, not architecture. (2) One noun per page. (3) If genuinely new: which sidebar group does
it belong to (REFERENCE_GROUPS, next bullet)? If none fits, propose a group to the user rather than
letting the page dangle ungrouped. (4) Sub-folders are exceptional — one entity = one page;
change-orders/ is the only folder today, and splitting a feature across a new folder needs the
user's sign-off first.
- Nav is TWO layers — miss the second and your page dangles at the bottom of the sidebar. Folders:
content/docs/{reference,platform,building,integrations}/. A folder's meta.json pages array is
authoritative for membership AND order (top-level section order is the root meta.json) — and the flat
Product-reference list is additionally nested into sidebar groups by the hardcoded REFERENCE_GROUPS
slug map in app/docs/layout.tsx, whose group order and within-group slug order are what the reader
sees. A reference page missing from that map is appended ungrouped after the groups. So adding a
reference page = slug into reference/meta.json and into the right REFERENCE_GROUPS group.
Don't list index in pages — fumadocs treats index.mdx as the folder index
and the nav renders it as "Overview"; listing it duplicates the title as a sibling. Integrations are
their own top-level section (content/docs/integrations/) grouped by category — document only active
integrations (omit active: false placeholders + commented-out ones).
- Components (
components/editorial/reference-components.tsx + components/mdx.tsx):
<Callout type="info|note|warn|warning|error|success|tip" title?>…</Callout> — type → badge+tone
(info/note→NOTE/blue, warn/warning/error→HEADS UP/amber, success/tip→GOOD TO KNOW/green). Note this
is a different Callout API than the Guides one (type vs tone+badge).
<Cards><Card title href icon?>…</Card></Cards> — link-card grid (cross-surface navigation).
<EnvVars><EnvVar name type? default? required?>…</EnvVar></EnvVars> — field/parameter rows. The env-var
page uses these; for an entity's field reference prefer <FieldTable> (below).
<FieldTable><Field name type? required?>desc</Field></FieldTable>
(components/editorial/field-table.tsx) — the accordion field/parameter reference, the warm-paper take on
Fumadocs' TypeTable. Use it for a | Field | Type | Description |-shaped table (item/job/line fields,
routing, work-center, reorder/shelf-life policy fields, integration settings). type = the type token
(omit when the table has no type column); required only when the source marks it so; the child is the
description as MDX (so inline `code`, italics, <Term> render — that's why it's children, not a
type={{}} prop). Registered in both mdx.tsx and editorial/mdx.tsx.
<StatusFlow entity="job"><Status name accent? branch? terminal?>meaning</Status></StatusFlow>
(components/editorial/status-flow.tsx) — an interactive lifecycle widget (selectable pills → a detail
Callout showing the meaning) that replaces a linear | Status | Meaning | table. Children in source
(lifecycle) order; meanings are MDX. Flags: accent = the single pivotal milestone (≤1, optional);
branch = a temporary returnable hold (Paused, On Hold, Needs Approval); terminal = an off-path exit
(Cancelled, Voided, Lost, Expired). entity is REQUIRED in practice: it resolves each status's
real ERP badge color from @carbon/utils/status-colors (statusColorMaps); omit it and every pill
silently renders gray — that is a bug, not a style. If the entity has no map yet, add one to
packages/utils/src/status-colors.ts, derived from the module's *Status.tsx component in the
ERP (never guessed), then pass it. Use it ONLY for a linear lifecycle — a 2-axis comparison matrix
(e.g. invoices sales-vs-purchase) or a small enum list stays a markdown table.
<AgentContext>…</AgentContext> — renders nothing on the site; its content ships only to the
in-app agent's knowledge base (pnpm run generate:agent-kb → apps/erp/app/modules/agent/kb/,
regenerated automatically by check-and-commit). Put troubleshooting/FAQ, exact error strings, and
internal mechanics (table names, routes, enum internals) here — reader-facing prose stays free of
them. Never write a reader-visible "Troubleshooting" section. About half the Reference pages still
have no <AgentContext>: when your fact sheet surfaced exact error strings for a page you're
editing, seed or extend its block while you're there.
<PlanBadge plan="Business" /> — flags a paid feature. Whole-page gate → set plan: Business in
frontmatter; renders a small "Paid" pill inline with the page title (label is fixed to "Paid"; the
plan value only feeds the hover tooltip + the banner copy). Section gate → drop <PlanBadge> in-body.
Gated features = packages/ee/src/plan.ts. Don't badge free ones (email, exchange-rates).
- Plan-gated pages also get a full-width announcement bar (
components/plan-banner-bar.tsx,
PlanBannerBar) rendered by the docs layout (app/docs/[[...slug]]/layout builds a url→plan map from
plan frontmatter and passes it in) — sticky under the header, spanning the content area. Adding
plan: Business frontmatter is all it takes; the badge and the bar both light up. Don't hand-place a
banner in MDX.
- Licensing model — get this right, it's been wrong before. See
docs/content/docs/platform/licensing.mdx
(the canonical page). Editions (packages/utils/src/types.ts enum Edition): Community (self-host,
AGPLv3 open core), Enterprise (self-host + commercial license), Cloud (managed). EE features =
code under packages/ee or any .ee file → require a commercial license per the repo LICENSE.
- Carbon Cloud = the hosted SaaS at https://app.carbon.ms — NOT the
/docs/platform/... deployment
docs. Self-hosting recipes (Docker with Caddy, AWS with SST) live under
docs/content/docs/platform/self-hosting/.
- Cloud plans = Starter and Business only.
Plan enum also has Partner, but Partner is
internal-only — never mention it in reader-facing docs. Don't write "Business and Partner plans".
- Never write "self-hosted isn't plan-gated." The runtime gate is Cloud-only
(
packages/ee/src/plan.server.ts returns early when CarbonEdition !== Edition.Cloud), but the
license still governs: self-hosting runs in community mode; Enterprise/EE features need a
commercial license. Frame it as a license boundary, not a technical lockout — features aren't
"disabled" in Community.
<Steps>/<Step>, <Tabs>/<Tab> (fumadocs-ui), markdown tables, and code fences (dark panel).
<Term id?>…</Term> — inline glossary term (dotted underline → definition popover). The same component
as the Guide; see "Interlinking & the glossary" below.
- Voice is more technical/scannable than the Guides — fields, constraints, tables — but still names the
gotcha and links back to the Guide for the narrative.
House voice
Calibrated by the page type chosen up front: Guides get the full register below; entity pages keep the
grounding and the gotchas but stay scannable; settings/how-to pages take only "quote real names exactly"
and "ground every where-to-click" — no narrative, no rhetorical asides, no answering questions nobody
asked. Cutting to budget beats compressing: drop whole asides, don't shorten every sentence.
- Active voice, second person, one term per concept. Never alternate synonyms for the same thing
("API key" one paragraph, "API token" the next) — the glossary term is the term, everywhere. No
colloquialisms or idioms; the catalogs are translated and idioms don't survive it.
- Never state the obvious. "Click Save to save" documents nothing. A step earns its place by
carrying something the button label doesn't already say.
- Short paragraphs — split at the idea boundary. A paragraph is one idea in 2–4 sentences
(~70 words). When a draft paragraph carries two ideas ("what it is" AND "how time splits back"),
break it in two rather than chaining clauses; a wall of prose is skipped, not read. Callout bodies
obey the same cap. The gate below flags any prose paragraph over ~480 characters.
- Every page stands alone. A reader (or an AI retriever) lands cold from search: state the context
and prerequisites in place rather than leaning on the previous page. How-to/settings pages name their
prerequisites explicitly before the steps ("needs
settings update permission", "requires the Slack
integration connected").
- Second person, concrete, narrative. Anchor in the running example (the 90-unit humanoid-robot order).
"Open the sales order dashboard." / "You don't build 90 robots as one monolithic job."
- Quote real status names exactly, in quotes:
**"To Ship and Invoice"**, **"Posted"**, **"Open"**.
Status strings live on a specific entity — confirm whether a value belongs to the header or the line
(e.g. "To Ship and Invoice"/"To Invoice" are salesOrderStatus, NOT the salesOrderLineStatus enum
Ordered/In Progress/Completed) before you attribute it, and state the transition fully (an order at
"To Ship and Invoice" flips to "To Invoice" once everything has shipped but isn't billed — don't imply
it sits at one value until fully closed).
- Em-dashes are rationed — count them. Hard budget: at most 1 per ~150 words of body prose (a 600-word
page gets 4, total), never two in one paragraph, never a dash-pair parenthetical in an opening sentence,
and none in
title=, caption=, or frontmatter strings. Default to a period, comma, or colon; most
dashes read better as two sentences. Do NOT mimic the dash-heavy style of this skill file or of older
pages — "too many em-dashes" is the single most common copy note this project gets. Before declaring a
page done, count (grep -o '—' <page>.mdx | wc -l) and cut to budget; when delegating prose to a
subagent, put the budget in its prompt verbatim.
- Keep the running example's nouns and numbers exact. It's the sales order (don't drift to a bare
"order" when instructing the reader) and it's 90 units (not "a robot"). Once you name the entity and the
quantity, stay consistent every time — drift is what makes a tour feel sloppy.
- Ground every "where to click." When you tell the reader to act in the UI, copy the real button / modal /
field labels from the JSX (in quotes) and confirm the capability exists on that exact path. A feature
that lives elsewhere is not the same as one on the screen you're describing — "Carbon splits the order into
three jobs" was wrong: the sales-order line's "Make to Order" → "Convert Line to Job" dialog makes
one job per click (the N-jobs-of-M split is a separate bulk-jobs flow). Verify the action, not just the
concept.
- Callouts carry the counterintuitive truth — the thing people get wrong. Title is a claim, body is the
why. ("Quotes are optional — the opportunity is the thread.")
- Answer the reader's cross-question where it arises. Every claim raises a next question in the reader's
head ("what if the job already started?", "does this touch existing orders?", "can I undo it?"). Anticipate
the one or two most likely at each step and answer them inline, at that point in the prose — a short
sentence, a Callout, or a
<Term> gloss — never deferred to a separate FAQ section or page. Ground the
answer in source like any other claim; if you can't answer it from code, that's a research gap, not a
skip. A section that leaves its obvious "but what about…" hanging isn't done.
- Explain the why, name the mistake, point to the next step. If a paragraph does none of those, cut it.
- Interlink at natural seams (
[make-to-order tour](/guides/order)).
- Lead, don't label. Don't open a page with a generic, repeated heading — no
## Introduction on guide
chapters (the chapter title is rendered for you) and no ## Why it matters on reference pages. Open with
1–2 substantive lead sentences, then go straight to the real sections. A heading that's identical on every
page is filler. Landing/index pages are "Overview", never "Introduction".
- Name things as they are now. Use a feature's current name; never narrate rename/removal history
("formerly item rules", "storage units used to…"). When an old name and the code disagree, the code wins.
Interlinking & the glossary
Two linking mechanisms — use both, deliberately, every time you touch a page. Internal linking compounds:
a page that links out and glosses its jargon is worth more than the same prose in isolation.
- Markdown links carry navigation. Link the noun, inline, at natural seams; never "click here". A Guide
links into Reference for fields; Reference links back to the Guide for the story
(
[make-to-order tour](/guides/order)). Cross-surface and cross-flow links are expected, not optional.
But link only when the destination's title echoes your anchor text. A hard jump whose landing page has
a different H1 disorients the reader — linking the phrase "quote to cash" to a chapter titled From quote
to order got flagged as confusing. When the anchor is a concept (a flow name, a category) rather than a
page literally called that, gloss it as a <Term> (definition popover + optional "Learn more" via href)
instead of a bare link — the reader gets the meaning in place and can still navigate if they choose.
<Term> carries definitions. Wrap a manufacturing/Carbon term a reader can hit cold (method type,
replenishment system, WIP, outside operation, kit/subassembly…) so a click gives the gloss without leaving
the page.
<Term>make to order</Term> slugifies the text to find the entry; <Term id="make-to-order">made</Term>
when the display text differs from the slug.
- First occurrence per page only — not every instance. Underlining every "order" is noise.
- Definitions are a single source of truth:
@carbon/glossary (packages/glossary/src/terms.ts),
where each entry is slug → { term, definition } with term/definition as Lingui msg descriptors
(rendered by docs/components/editorial/glossary.tsx).
Add the entry there before you use a new term, and ground the definition in source (the prime
directive applies — exact enum values, real behavior). Omit href when there's no dedicated page yet (the
popover still shows the definition); the "Learn more" link auto-hides when it would point at the page
you're already on.
- Unknown slug → renders as plain text (never breaks prose) — a typo fails safe, not loud.
Enrichment pass. Whenever you create or edit a page, finish with a linking pass: wrap first-occurrence
jargon in <Term>, add markdown cross-links at the seams, and add any missing glossary entries. Cheapest way
to raise the whole site's connectivity.
Design / styling
- Light-only. Warm-paper palette: page bg
#FBFBF9 / #F5F5F2; ink #262323 (+ rgba(38,35,35,0.x)
for faint); accent #1E84B0 (links) / #00B0FF (focus/brand); hairlines #E7E7E3 / #E3E3DF.
- Inline Tailwind arbitrary values (
text-[15px], bg-[#FBFBF9], border-[#E7E7E3]) — this app is
standalone (NOT @carbon/react, NOT the ERP theme). Match the surrounding component's density and colors;
don't introduce a new palette.
- Fonts: DM Sans (body), Fira Code (mono). Callout tone fills/borders: neutral
#EFEFEB/#DADAD5,
blue #DFF5FF/#A9DAF3, green #E4F8DA/#A8DB91, amber #FFF2D8/#E6CFA3.
Gotchas (hard-won)
- Frontmatter is YAML — never leave an unquoted colon in a value. A
title:/description: value
containing : (colon-space) is parsed as a nested mapping and throws
YAMLException: bad indentation of a mapping entry. Either quote the whole value
(description: "How it works: the short version.") or reword to drop the colon (a comma/period is usually
the better fix). One bad frontmatter 500s the entire site, not just that page — fumadocs loads the
whole source collection at module load, so the symptom is every page (guides included) returning 500 with
the offending .mdx path in the stack. This bites hardest during an em-dash sweep: replacing a
description: em-dash with a colon silently breaks YAML. pnpm exec fumadocs-mdx regen does NOT catch
it (it passes) — the only reliable check is fetching a page (any page) and seeing 200 vs 500. When you
delegate prose edits to subagents, tell them explicitly: in frontmatter, never introduce an unquoted colon.
- Shiki theme (
source.config.ts → rehypeCodeOptions.themes): set both light and dark to the
same "github-dark-default". A single theme (or a missing github-light) breaks the build for any
content/docs file with a code fence (ShikiError: Theme github-light not found). Guides have no fences,
so they're immune — handy for isolating a red build to the Reference side.
- Regen
.source before typecheck after any frontmatter/schema change (the schema is baked into
.source/ at generate time).
- Figure keys must exist — see the list above; a typo renders nothing, silently.
- Inline code chips are for identifiers, never statements. A full command, SQL statement, or long
path in an inline
`…` span renders as one unbroken chip that can overflow its container —
anything that reads as a line of code goes in a fenced block (fences are fine inside Callouts),
and inline stays for short tokens (companyId, crbn_…, Settings → X labels stay plain bold).
- Bare
{…} in MDX is a JS expression. A token in prose like {item.id} (e.g. inside an example rule
message) fails the build with item is not defined. Wrap any literal braces/tokens in backticks: `{item.id}`.
Write blocks on existing files — natural collision protection when a parallel session co-writes the
docs. Just write the next uncovered gap; don't pause to coordinate unless asked.
curl GET can 405 in this sandbox (a method-less request reads as POST) — use the WebFetch tool for
external pages; the pnpm registry works fine.
- Don't hand-edit the API reference (generated) — change the generator/schema and rebuild.
Deep-dive references (read when you reach the relevant phase)
references/components.md — full component APIs for both surfaces + the illustration registry.
references/writing-guide.md — the voice, the grounding rule, worked examples.
references/information-architecture.md — the flow IA and the Reference meta.json nav.
references/design-language.md — palette, fonts, the inline-Tailwind convention.
Note: references/scaffold.md, references/brand-integration.md, and assets/templates/* are
scaffolding-era (how the app was first stood up, with Geist/@carbon/react/signature-touch templates).
The app diverged from them. The live components in docs/components/{editorial,api}/ are the source
of truth — read those, not the templates, when in doubt.
Verification bar
Mechanical gate — run on every touched page BEFORE the re-read; each must come back clean. These are
deterministic, so there is no judgment call to get lazy on:
Measure READER-VISIBLE content only — raw wc -w counts frontmatter and the invisible <AgentContext>
block too, overstating a page by up to 40% and making budgets meaningless:
awk '/<AgentContext>/{s=1} !s{print} /<\/AgentContext>/{s=0}' <page>.mdx | wc -w # words vs the type's budget
awk '/<AgentContext>/{s=1} !s{print} /<\/AgentContext>/{s=0}' <page>.mdx | grep -o '—' | wc -l # em-dashes ≤ 1 per ~150 words
grep -n '<StatusFlow>' <page>.mdx # bare = gray pills; must carry entity="…"
grep -nE '^(title|description): [^"]*: ' <page>.mdx # unquoted colon-space = YAML, 500s the site
grep -m1 '^description:' <page>.mdx | wc -c # ≤ ~170 raw (the SEO meta: 150–160 chars of text)
awk '/<AgentContext>/{s=1} !s{print} /<\/AgentContext>/{s=0}' <page>.mdx | grep -vE '^\s*([<#|`-]|$)' | awk 'length>480 {print "long paragraph: " substr($0,1,60) "…"}' # split any hit at its idea boundary
Plus two eyeball checks no grep covers: a ## Troubleshooting heading may exist only inside
<AgentContext>, and every <Figure illustration=…> key is in the registry list.
Never declare docs done without: the mechanical gate passing, the new content rendering (in the user's
running dev server, or a clean pnpm --filter docs build), every internal link resolving, any new
<Term> glossary entries grounded in source + their popovers rendering, names matching real code,
no generic repeated headings, and a re-read that confirms each page says what matters / names the
mistake / points onward.
Close with a media manifest. The final report to the user MUST end with the list of images/media the
work needs captured — every unfilled <Screenshot> slot on a page you created or touched, one line each:
page path — slot label (ratio) — what to capture (screen + state). Build it with
grep -n '<Screenshot' <touched pages>. That list is the user's capture worklist; a slot you don't
report is a slot that never gets filled. No new slots → say "no new media needed" explicitly. Then
record progress (the .ai/plans/ plan file if one exists, + memory). Don't kill or rebuild under the user's running dev server.
1---2name: carbon-docs3description: Author, edit, or extend the Carbon documentation site at `docs` (a built Fumadocs + Next.js app). Use whenever creating or changing reader-facing docs for Carbon: editorial Guide chapters, Reference/entity pages, the docs IA, or "document this feature" requests — anything destined for the docs site. Covers the grounded-in-source authoring workflow, the flow-based Guide architecture, the real MDX components for each surface, the warm-paper house style, and the build verification loop. Trigger even if the user doesn't say "docs" but clearly wants explanatory, reader-facing written material about how Carbon works.4---56# Carbon Docs78`docs` is a **built, opinionated** documentation site — Fumadocs + Next.js (React 19), **light-only**,9warm-paper aesthetic. It is no longer scaffolded; it exists and ships content. This skill is how to add10or change docs **in that system, in its house style, grounded in real Carbon code.**1112**Announce at start:** "Using the carbon-docs skill — authoring docs for {topic}."1314## First: ask which kind of page this is1516Not all doc pages are equal — a settings toggle does not get the treatment a production flow gets, and17the single biggest quality failure so far has been writing every page in the long narrative register.18**Before any research or writing, ask the user to pick the page type** (AskUserQuestion, one question,19your recommended type first — infer it from the topic). Skip the question only when the user already20named the type in their request, or you're editing an existing page without changing its shape (keep21its current type). Several pages in one request → one question covering them.2223| Type | Reader's question | Shape | Budget |24|---|---|---|---|25| **Flow / tour** (Guide) | "walk me through it" | Narrative chapter in a flow: sequenced, illustrated, second-person storytelling, prior stages set up | length serves the story |26| **System / entity** (Reference) | "how does it work — fields, statuses, rules?" | 1–2 lead sentences → `<StatusFlow entity=…>` lifecycle → rule/gotcha Callouts → `<FieldTable>` → `<Cards>` onward | ~800–1,500 words |27| **Settings / how-to** (Reference) | "how do I turn it on / use it?" | 1–2 lead sentences → `<Steps>` → one gotcha Callout → where-it-lives | ~300–600 words; steps, not story |28| **Concept / overview** (Reference) | "what is this area?" | Short intro + `<Cards>` linking to the owning pages | ~150–300 words |2930A page can mix types **by section** (an entity page that ends with a short settings section) — pick the31dominant type for the page, then hold each section to its own shape. Troubleshooting/FAQ content never32gets a reader-visible section: it goes in `<AgentContext>` (see Reference components).3334**No page ends cold.** Every page closes pointing onward: entity and concept pages with a small35`<Cards>` Related row, settings pages with where-it-lives links, guide chapters get "read next" for36free. A page that just stops is an IA bug, not a style choice.3738The house voice below scales with this choice: full narrative register belongs to Guides; entity pages39dial it down to scannable-with-gotchas; settings/how-to pages drop it entirely. On a settings page the40reader wants the steps, not an essay — two-factor auth is a 500-word how-to, not an 1,800-word41meditation on passwords.4243> The single biggest mistake is writing plausible ERP-generic prose. Carbon's behavior is specific and44> often counterintuitive (WIP is a GL balance not a table; a payment is a posted entity applied via45> settlements; overhead IS absorbed into WIP per production event; fixed-asset disposal is46> scrapping-only). **Every claim is grounded in source.** See the47> prime directive below — it overrides everything.4849> **What Carbon is:** a manufacturing system — **ERP** (the office) + **MES** (the floor), one platform over50> one data model. **Academy is *not* a third product pillar**: it just hosts training **video lessons**, not a51> SaaS app a customer buys. Never frame Carbon as "ERP, MES, and training/Academy"; site, meta, and landing52> copy stay **ERP + MES**. (The `academy` app can appear in an architecture/monorepo listing — fine; describe53> it as video-lesson hosting, not a pillar.)5455## The prime directive — ground everything in source5657The **holy source of truth is the actual source code + the LATEST database migrations**58(`packages/database/supabase/migrations/`, newest by timestamp) — NOT ERP/CMMS general knowledge, NOT59`.claude/rules/` alone (it is often stale).6061- **Verify before you write.** Every entity, status enum *value*, and transition named in docs must exist62 in real code. Confirm exact strings (`"To Ship and Invoice"`, `"Fully Depreciated"`), the actions that63 drive them (service fns, routes, edge functions in `packages/database/supabase/functions/`), and what64 posts/gates (e.g. `companySettings.accountingEnabled`).65- **Read the newest migration, not the first.** Timestamps order them; a 2026 refactor may have rebuilt a66 subsystem the cache still describes the old way.67- **Document only real, ACTIVE features.** Omit placeholders / inactive / not-yet-shipped things (e.g.68 integration registry entries with `active: false` like QuickBooks/Sage). Don't surface them.69- **Deliberately undocumented:** the **Implementation Hub** (`/x/get-started`, `packages/onboarding`70 content) stays out of the docs by the user's decision — don't document it, and don't link to it.71- **When code and cache disagree, code wins** — and note the drift.72- **Method:** one research subagent per feature → a structured **fact sheet** with `file:line` refs73 (contract in Authoring workflow step 1) → then write from it. This is how every flow in the Guide was74 built. Don't skip it for anything non-trivial.7576## Three surfaces (know which you're touching)7778| Surface | Path / route | What it is | Authored? |79|---|---|---|---|80| **Guides** | `content/guides/*.mdx` → `/guides` | Editorial narrative *tours*, grouped into **flows**. Second-person, opinionated, illustrated. The journey + the "why". | Hand-written MDX |81| **Reference** | `content/docs/**/*.mdx` → `/docs` | One page per entity/concept. Scannable: tables, cards, field rows. The "what". | Hand-written MDX |82| **API reference** | `app/api-reference/[module]/[resource]` | PostgREST endpoint docs, **generated at build** from the swagger. | **Generated — do NOT hand-edit.** Edit `scripts/generate-api-docs.mjs` or the swagger schema. |8384Interlink across surfaces and flows: a Guide links into Reference for detail; Reference links back to the85Guide for the story. (Guide chapters were given 12 cross-flow links — interlinking is expected, not optional.)8687## Authoring workflow (every change)88891. **Research (grounded) → a fact sheet.** Dispatch a research subagent per feature. It must return a90 structured **fact sheet**, not prose: entities/tables/columns; exact status strings and which entity91 owns each; transitions with the action that drives them (service fn / route / edge function) as92 `file:line`; gates and settings (`companySettings.*`, plan features); the real UI labels for every93 where-to-click; the gotchas a generic description would get wrong; and an explicit "could not verify"94 list — a gap on that list is more research work, never a writing improvisation. The writer then works95 ONLY from the fact sheet + the page-type template, and writing stays in the main loop on the main96 model — no model downgrades for prose; the fact sheet is the handoff, not a license for a weaker97 writer.982. **Pick the surface + placement** (the page *type* was chosen up front). Guide flow + frontmatter, or99 Reference folder + `meta.json` + `REFERENCE_GROUPS` — run the placement checklist below before100 creating any new page.1013. **Write in the house voice** with the surface's real components (below). Lead with a concrete example.102 **Decide media placement yourself as you write — don't wait to be asked.** Drop a `<Screenshot>` slot103 wherever the reader needs to see the actual UI, with a label naming the real screen + state (the fact104 sheet's UI labels are the source); use `<Figure>` only when a registry key genuinely fits. Typical105 density by page type: settings/how-to 1 (the screen with the switch/steps), entity 1–2 (the list or106 detail view, or the lifecycle UI), guide chapters one per major beat, concept stubs none. **Boy Scout107 rule:** editing an existing Reference page that has no `<Screenshot>`? Add the one slot its type calls108 for while you're there — the Reference surface shipped with almost none, and it goes on the media109 manifest like any new slot. Reuse existing slots on a rewrite; never drop or re-label one that a real110 capture may already exist for. Slot `label`s double as the image's alt text once a capture lands, so111 write them as a description of what's *shown* (screen + state), never as a caption or a filename.1124. **Verify — against the user's running dev server, read-only.** They usually have `pnpm --filter docs dev`113 up. **Never** `pkill`/restart it, run `next build`, or `rm .next` under it — verify by fetching pages:114 ```bash115 cd docs116 pnpm exec fumadocs-mdx # regen .source if a content/frontmatter change didn't HMR117 curl -sS -X GET http://localhost:3002/docs/<slug> | grep -o "your new heading" # confirm it rendered118 ```119 A green `pnpm --filter docs build` (`✓ Generating static pages N/N`) is the gold standard — but only in a120 clean checkout / when no dev server is running. `tsc` is noisy (a React 18/19 `@types` skew that121 `next.config` ignores) — grep your own files out of the errors rather than expecting zero.122123## Guides — the flow architecture124125Frontmatter (`source.config.ts` schema):126127```yaml128---129title: From quote to order130description: An RFQ becomes a quote; an accepted quote becomes a sales order.131label: "(I)" # display marker, roman numeral, per-flow132index: 0 # order WITHIN the flow (0,1,2…)133flow: quote-to-cash # flow id (omit → defaults to "make-to-order")134flowName: Quote to cash # flow tab label135flowIndex: 1 # order of the flow in the subnav (0 = first)136---137```138139- Chapters sort by `(flowIndex, index)`. The **subnav is a flow switcher**; the **sidebar + mobile selector140 + "read next" are scoped to the active flow**. The original 5 chapters (order/build/plan/floor/ship) carry141 no flow fields and fold into `make-to-order` (flowIndex 0) via defaults.142- **Add a chapter to a flow:** same `flow`/`flowName`/`flowIndex`, next `index` + `label`.143- **Add a flow:** new `flowIndex` + `flowName`; its chapters start at `index: 0`, `label: "(I)"`.144- All chapter bodies render server-side on every page (the reader cross-fades, no route nav), so links to145 `/guides/<slug>` resolve. **No code fences in guides** (prose + components only).146147**Components** (`components/editorial/mdx.tsx`):148149- `<Figure illustration="flow-overview" caption="…" />` — SVG from the registry. **`illustration` MUST be a150 real key** from `components/editorial/illustrations.tsx` (else it silently renders nothing). Valid keys:151 `flow-overview, order-split, bom-tree, demand-forecast, planning-engine, shopfloor-loop, eight-d,152 traceability-graph, method-types, kit-vs-subassembly, reorder-policy, outside-processing, mes-station,153 issue-workflow, schedule-board, get-method, conversion-factor, opportunity-thread, cash-cycle,154 rfq-fanout, receive-bill-axes, wip-inflow, wip-to-cogs, depreciation-curve, asset-exit`. For anything155 without a fitting key, use `<Screenshot>` instead — don't invent keys.156- `<Screenshot label="Sales order dashboard" caption="…" ratio="wide|tall|square" />` — a standing **slot for157 a real Carbon capture**. Use it where the reader needs to see the actual UI (dashboard, a specific158 form/field, status, board, where-to-click) — not as decoration. Label the **real, current** screen + state159 precisely (verify it exists) so a real screenshot can drop straight in and the docs stay synced to the live160 Carbon UI. Default `ratio="wide"`.161- `<Callout tone="neutral|blue|green|amber" badge="WHY BATCH" title="…">body</Callout>` — the workhorse.162 Use it to carry the **Carbon-specific truth a generic description gets wrong**. Tones: blue = explanatory163 "why", green = good-to-know/outcome, amber = caution/contrast, neutral = definitional.164- `<Divider />` — closes a chapter before its wrap-up line.165- `<Term>make to order</Term>` — inline glossary term: dotted-underline; click/tap opens a popover with a166 grounded one-line definition + an optional "Learn more" link. Same component on both surfaces; definitions167 live in `@carbon/glossary` (`packages/glossary/src/terms.ts`), rendered by168 `docs/components/editorial/glossary.tsx`. See "Interlinking & the glossary" below.169170Each `##` heading becomes a sidebar rail entry — so structure chapters as 3–5 `##` sections.171172## Reference — entity pages173174- Frontmatter: `title` + `description` (+ `plan` for a paid-feature gate).175- **Placement checklist — run it BEFORE creating any page.** (1) Does an existing page already own this176 topic? Grep `content/docs` for the feature name; if a page covers it, **extend that page** with a177 section instead of creating a parallel one — a bucket page that re-documents features owned elsewhere178 is scattering, not architecture. (2) One noun per page. (3) If genuinely new: which sidebar group does179 it belong to (`REFERENCE_GROUPS`, next bullet)? If none fits, propose a group to the user rather than180 letting the page dangle ungrouped. (4) **Sub-folders are exceptional** — one entity = one page;181 `change-orders/` is the only folder today, and splitting a feature across a new folder needs the182 user's sign-off first.183- **Nav is TWO layers — miss the second and your page dangles at the bottom of the sidebar.** Folders:184 `content/docs/{reference,platform,building,integrations}/`. A folder's `meta.json` `pages` array is185 authoritative for membership AND order (top-level section order is the root `meta.json`) — and the flat186 Product-reference list is additionally nested into sidebar groups by the hardcoded `REFERENCE_GROUPS`187 slug map in `app/docs/layout.tsx`, whose group order and within-group slug order are what the reader188 sees. A reference page missing from that map is appended **ungrouped** after the groups. So adding a189 reference page = slug into `reference/meta.json` **and** into the right `REFERENCE_GROUPS` group.190 **Don't list `index` in `pages`** — fumadocs treats `index.mdx` as the folder index191 and the nav renders it as **"Overview"**; listing it duplicates the title as a sibling. Integrations are192 their own top-level section (`content/docs/integrations/`) grouped by category — document only `active`193 integrations (omit `active: false` placeholders + commented-out ones).194- **Components** (`components/editorial/reference-components.tsx` + `components/mdx.tsx`):195 - `<Callout type="info|note|warn|warning|error|success|tip" title?>…</Callout>` — type → badge+tone196 (`info/note`→NOTE/blue, `warn/warning/error`→HEADS UP/amber, `success/tip`→GOOD TO KNOW/green). Note this197 is a **different Callout API** than the Guides one (type vs tone+badge).198 - `<Cards><Card title href icon?>…</Card></Cards>` — link-card grid (cross-surface navigation).199 - `<EnvVars><EnvVar name type? default? required?>…</EnvVar></EnvVars>` — field/parameter rows. The env-var200 page uses these; for an entity's **field reference** prefer `<FieldTable>` (below).201 - `<FieldTable><Field name type? required?>desc</Field></FieldTable>`202 (`components/editorial/field-table.tsx`) — the accordion field/parameter reference, the warm-paper take on203 Fumadocs' TypeTable. Use it for a `| Field | Type | Description |`-shaped table (item/job/line fields,204 routing, work-center, reorder/shelf-life policy fields, integration settings). `type` = the type token205 (omit when the table has no type column); `required` only when the source marks it so; the child is the206 description as **MDX** (so inline `` `code` ``, *italics*, `<Term>` render — that's why it's children, not a207 `type={{}}` prop). Registered in **both** `mdx.tsx` and `editorial/mdx.tsx`.208 - `<StatusFlow entity="job"><Status name accent? branch? terminal?>meaning</Status></StatusFlow>`209 (`components/editorial/status-flow.tsx`) — an **interactive** lifecycle widget (selectable pills → a detail210 `Callout` showing the meaning) that replaces a linear `| Status | Meaning |` table. Children in source211 (lifecycle) order; meanings are MDX. Flags: `accent` = the single pivotal milestone (≤1, optional);212 `branch` = a temporary returnable hold (Paused, On Hold, Needs Approval); `terminal` = an off-path exit213 (Cancelled, Voided, Lost, Expired). **`entity` is REQUIRED in practice**: it resolves each status's214 real ERP badge color from `@carbon/utils/status-colors` (`statusColorMaps`); omit it and every pill215 silently renders gray — that is a bug, not a style. If the entity has no map yet, add one to216 `packages/utils/src/status-colors.ts`, **derived from the module's `*Status.tsx` component in the217 ERP** (never guessed), then pass it. Use it ONLY for a **linear** lifecycle — a 2-axis comparison matrix218 (e.g. invoices sales-vs-purchase) or a small enum list stays a markdown table.219 - `<AgentContext>…</AgentContext>` — renders **nothing** on the site; its content ships only to the220 in-app agent's knowledge base (`pnpm run generate:agent-kb` → `apps/erp/app/modules/agent/kb/`,221 regenerated automatically by check-and-commit). Put troubleshooting/FAQ, exact error strings, and222 internal mechanics (table names, routes, enum internals) here — reader-facing prose stays free of223 them. Never write a reader-visible "Troubleshooting" section. About half the Reference pages still224 have no `<AgentContext>`: when your fact sheet surfaced exact error strings for a page you're225 editing, seed or extend its block while you're there.226 - `<PlanBadge plan="Business" />` — flags a paid feature. Whole-page gate → set `plan: Business` in227 frontmatter; renders a small **"Paid"** pill inline with the page title (label is fixed to "Paid"; the228 `plan` value only feeds the hover tooltip + the banner copy). Section gate → drop `<PlanBadge>` in-body.229 Gated features = `packages/ee/src/plan.ts`. **Don't** badge free ones (email, exchange-rates).230 - **Plan-gated pages also get a full-width announcement bar** (`components/plan-banner-bar.tsx`,231 `PlanBannerBar`) rendered by the docs **layout** (`app/docs/[[...slug]]/layout` builds a url→plan map from232 `plan` frontmatter and passes it in) — sticky under the header, spanning the content area. Adding233 `plan: Business` frontmatter is all it takes; the badge and the bar both light up. Don't hand-place a234 banner in MDX.235 - **Licensing model — get this right, it's been wrong before.** See `docs/content/docs/platform/licensing.mdx`236 (the canonical page). Editions (`packages/utils/src/types.ts` `enum Edition`): **Community** (self-host,237 AGPLv3 open core), **Enterprise** (self-host + commercial license), **Cloud** (managed). EE features =238 code under `packages/ee` or any `.ee` file → require a commercial license per the repo `LICENSE`.239 - **Carbon Cloud = the hosted SaaS at https://app.carbon.ms** — NOT the `/docs/platform/...` deployment240 docs. Self-hosting recipes (Docker with Caddy, AWS with SST) live under241 `docs/content/docs/platform/self-hosting/`.242 - **Cloud plans = Starter and Business only.** `Plan` enum also has `Partner`, but Partner is243 **internal-only — never mention it in reader-facing docs.** Don't write "Business and Partner plans".244 - **Never write "self-hosted isn't plan-gated."** The *runtime* gate is Cloud-only245 (`packages/ee/src/plan.server.ts` returns early when `CarbonEdition !== Edition.Cloud`), but the246 *license* still governs: self-hosting runs in **community mode**; Enterprise/EE features need a247 commercial license. Frame it as a license boundary, not a technical lockout — features aren't248 "disabled" in Community.249 - `<Steps>/<Step>`, `<Tabs>/<Tab>` (fumadocs-ui), markdown tables, and code fences (dark panel).250 - `<Term id?>…</Term>` — inline glossary term (dotted underline → definition popover). The same component251 as the Guide; see "Interlinking & the glossary" below.252- Voice is more technical/scannable than the Guides — fields, constraints, tables — but still names the253 gotcha and links back to the Guide for the narrative.254255## House voice256257Calibrated by the page type chosen up front: Guides get the full register below; entity pages keep the258grounding and the gotchas but stay scannable; settings/how-to pages take only "quote real names exactly"259and "ground every where-to-click" — no narrative, no rhetorical asides, no answering questions nobody260asked. Cutting to budget beats compressing: drop whole asides, don't shorten every sentence.261262- **Active voice, second person, one term per concept.** Never alternate synonyms for the same thing263 ("API key" one paragraph, "API token" the next) — the glossary term is the term, everywhere. No264 colloquialisms or idioms; the catalogs are translated and idioms don't survive it.265- **Never state the obvious.** "Click Save to save" documents nothing. A step earns its place by266 carrying something the button label doesn't already say.267- **Short paragraphs — split at the idea boundary.** A paragraph is one idea in 2–4 sentences268 (~70 words). When a draft paragraph carries two ideas ("what it is" AND "how time splits back"),269 break it in two rather than chaining clauses; a wall of prose is skipped, not read. Callout bodies270 obey the same cap. The gate below flags any prose paragraph over ~480 characters.271- **Every page stands alone.** A reader (or an AI retriever) lands cold from search: state the context272 and prerequisites in place rather than leaning on the previous page. How-to/settings pages name their273 prerequisites explicitly before the steps ("needs `settings` update permission", "requires the Slack274 integration connected").275- **Second person, concrete, narrative.** Anchor in the running example (the 90-unit humanoid-robot order).276 "Open the sales order dashboard." / "You don't build 90 robots as one monolithic job."277- **Quote real status names exactly**, in quotes: `**"To Ship and Invoice"**`, `**"Posted"**`, `**"Open"**`.278 Status strings live on a specific entity — confirm whether a value belongs to the *header* or the *line*279 (e.g. `"To Ship and Invoice"`/`"To Invoice"` are `salesOrderStatus`, NOT the `salesOrderLineStatus` enum280 `Ordered/In Progress/Completed`) before you attribute it, and state the transition fully (an order at281 `"To Ship and Invoice"` flips to `"To Invoice"` once everything has shipped but isn't billed — don't imply282 it sits at one value until fully closed).283- **Em-dashes are rationed — count them.** Hard budget: at most 1 per ~150 words of body prose (a 600-word284 page gets 4, total), never two in one paragraph, never a dash-pair parenthetical in an opening sentence,285 and none in `title=`, `caption=`, or frontmatter strings. Default to a period, comma, or colon; most286 dashes read better as two sentences. Do NOT mimic the dash-heavy style of this skill file or of older287 pages — "too many em-dashes" is the single most common copy note this project gets. Before declaring a288 page done, count (`grep -o '—' <page>.mdx | wc -l`) and cut to budget; when delegating prose to a289 subagent, put the budget in its prompt verbatim.290- **Keep the running example's nouns and numbers exact.** It's the *sales order* (don't drift to a bare291 "order" when instructing the reader) and it's *90 units* (not "a robot"). Once you name the entity and the292 quantity, stay consistent every time — drift is what makes a tour feel sloppy.293- **Ground every "where to click."** When you tell the reader to act in the UI, copy the real button / modal /294 field labels from the JSX (in quotes) and confirm the capability exists *on that exact path*. A feature295 that lives elsewhere is not the same as one on the screen you're describing — "Carbon splits the order into296 three jobs" was wrong: the sales-order line's **"Make to Order"** → **"Convert Line to Job"** dialog makes297 **one** job per click (the N-jobs-of-M split is a separate bulk-jobs flow). Verify the action, not just the298 concept.299- **Callouts carry the counterintuitive truth** — the thing people get wrong. Title is a claim, body is the300 why. ("Quotes are optional — the opportunity is the thread.")301- **Answer the reader's cross-question where it arises.** Every claim raises a next question in the reader's302 head ("what if the job already started?", "does this touch existing orders?", "can I undo it?"). Anticipate303 the one or two most likely at each step and answer them **inline, at that point in the prose** — a short304 sentence, a Callout, or a `<Term>` gloss — never deferred to a separate FAQ section or page. Ground the305 answer in source like any other claim; if you can't answer it from code, that's a research gap, not a306 skip. A section that leaves its obvious "but what about…" hanging isn't done.307- **Explain the why, name the mistake, point to the next step.** If a paragraph does none of those, cut it.308- **Interlink** at natural seams (`[make-to-order tour](/guides/order)`).309- **Lead, don't label.** Don't open a page with a generic, repeated heading — no `## Introduction` on guide310 chapters (the chapter title is rendered for you) and no `## Why it matters` on reference pages. Open with311 1–2 substantive lead sentences, then go straight to the real sections. A heading that's identical on every312 page is filler. Landing/index pages are **"Overview"**, never "Introduction".313- **Name things as they are now.** Use a feature's current name; never narrate rename/removal history314 ("formerly item rules", "storage units used to…"). When an old name and the code disagree, the code wins.315316## Interlinking & the glossary317318Two linking mechanisms — use **both**, deliberately, every time you touch a page. Internal linking compounds:319a page that links out *and* glosses its jargon is worth more than the same prose in isolation.320321- **Markdown links carry navigation.** Link the *noun*, inline, at natural seams; never "click here". A Guide322 links into Reference for fields; Reference links back to the Guide for the story323 (`[make-to-order tour](/guides/order)`). Cross-surface and cross-flow links are expected, not optional.324 **But link only when the destination's title echoes your anchor text.** A hard jump whose landing page has325 a different H1 disorients the reader — linking the phrase "quote to cash" to a chapter titled *From quote326 to order* got flagged as confusing. When the anchor is a *concept* (a flow name, a category) rather than a327 page literally called that, gloss it as a `<Term>` (definition popover + optional "Learn more" via `href`)328 instead of a bare link — the reader gets the meaning in place and can still navigate if they choose.329- **`<Term>` carries definitions.** Wrap a manufacturing/Carbon term a reader can hit cold (method type,330 replenishment system, WIP, outside operation, kit/subassembly…) so a click gives the gloss without leaving331 the page.332 - `<Term>make to order</Term>` slugifies the text to find the entry; `<Term id="make-to-order">made</Term>`333 when the display text differs from the slug.334 - **First occurrence per page only** — not every instance. Underlining every "order" is noise.335 - Definitions are a single source of truth: `@carbon/glossary` (`packages/glossary/src/terms.ts`),336 where each entry is `slug → { term, definition }` with `term`/`definition` as Lingui `msg` descriptors337 (rendered by `docs/components/editorial/glossary.tsx`).338 Add the entry there *before* you use a new term, and **ground the definition in source** (the prime339 directive applies — exact enum values, real behavior). Omit `href` when there's no dedicated page yet (the340 popover still shows the definition); the "Learn more" link auto-hides when it would point at the page341 you're already on.342 - Unknown slug → renders as plain text (never breaks prose) — a typo fails safe, not loud.343344**Enrichment pass.** Whenever you create or edit a page, finish with a linking pass: wrap first-occurrence345jargon in `<Term>`, add markdown cross-links at the seams, and add any missing glossary entries. Cheapest way346to raise the whole site's connectivity.347348## Design / styling349350- **Light-only.** Warm-paper palette: page bg `#FBFBF9` / `#F5F5F2`; ink `#262323` (+ `rgba(38,35,35,0.x)`351 for faint); accent `#1E84B0` (links) / `#00B0FF` (focus/brand); hairlines `#E7E7E3` / `#E3E3DF`.352- **Inline Tailwind arbitrary values** (`text-[15px]`, `bg-[#FBFBF9]`, `border-[#E7E7E3]`) — this app is353 standalone (NOT `@carbon/react`, NOT the ERP theme). Match the surrounding component's density and colors;354 don't introduce a new palette.355- Fonts: DM Sans (body), Fira Code (mono). Callout tone fills/borders: neutral `#EFEFEB/#DADAD5`,356 blue `#DFF5FF/#A9DAF3`, green `#E4F8DA/#A8DB91`, amber `#FFF2D8/#E6CFA3`.357358## Gotchas (hard-won)359360- **Frontmatter is YAML — never leave an unquoted colon in a value.** A `title:`/`description:` value361 containing `: ` (colon-space) is parsed as a nested mapping and throws362 `YAMLException: bad indentation of a mapping entry`. Either quote the whole value363 (`description: "How it works: the short version."`) or reword to drop the colon (a comma/period is usually364 the better fix). **One bad frontmatter 500s the *entire* site, not just that page** — fumadocs loads the365 whole source collection at module load, so the symptom is every page (guides included) returning 500 with366 the offending `.mdx` path in the stack. This bites hardest during an em-dash sweep: replacing a367 `description:` em-dash with a colon silently breaks YAML. **`pnpm exec fumadocs-mdx` regen does NOT catch368 it** (it passes) — the only reliable check is fetching a page (any page) and seeing 200 vs 500. When you369 delegate prose edits to subagents, tell them explicitly: in frontmatter, never introduce an unquoted colon.370- **Shiki theme** (`source.config.ts` → `rehypeCodeOptions.themes`): set **both** `light` and `dark` to the371 same `"github-dark-default"`. A single `theme` (or a missing `github-light`) breaks the build for any372 `content/docs` file with a code fence (`ShikiError: Theme github-light not found`). Guides have no fences,373 so they're immune — handy for isolating a red build to the Reference side.374- **Regen `.source` before typecheck** after any frontmatter/schema change (the schema is baked into375 `.source/` at generate time).376- **Figure keys must exist** — see the list above; a typo renders nothing, silently.377- **Inline code chips are for identifiers, never statements.** A full command, SQL statement, or long378 path in an inline `` `…` `` span renders as one unbroken chip that can overflow its container —379 anything that reads as a *line of code* goes in a fenced block (fences are fine inside Callouts),380 and inline stays for short tokens (`companyId`, `crbn_…`, `Settings → X` labels stay plain bold).381- **Bare `{…}` in MDX is a JS expression.** A token in prose like `{item.id}` (e.g. inside an example rule382 message) fails the build with `item is not defined`. Wrap any literal braces/tokens in backticks: `` `{item.id}` ``.383- **`Write` blocks on existing files** — natural collision protection when a parallel session co-writes the384 docs. Just write the next uncovered gap; don't pause to coordinate unless asked.385- **`curl` GET can 405 in this sandbox** (a method-less request reads as POST) — use the WebFetch tool for386 external pages; the pnpm registry works fine.387- **Don't hand-edit the API reference** (generated) — change the generator/schema and rebuild.388389## Deep-dive references (read when you reach the relevant phase)390391- `references/components.md` — full component APIs for both surfaces + the illustration registry.392- `references/writing-guide.md` — the voice, the grounding rule, worked examples.393- `references/information-architecture.md` — the flow IA and the Reference `meta.json` nav.394- `references/design-language.md` — palette, fonts, the inline-Tailwind convention.395396> Note: `references/scaffold.md`, `references/brand-integration.md`, and `assets/templates/*` are397> **scaffolding-era** (how the app was first stood up, with Geist/`@carbon/react`/signature-touch templates).398> The app diverged from them. **The live components in `docs/components/{editorial,api}/` are the source399> of truth** — read those, not the templates, when in doubt.400401## Verification bar402403**Mechanical gate — run on every touched page BEFORE the re-read; each must come back clean.** These are404deterministic, so there is no judgment call to get lazy on:405406Measure READER-VISIBLE content only — raw `wc -w` counts frontmatter and the invisible `<AgentContext>`407block too, overstating a page by up to 40% and making budgets meaningless:408409```bash410awk '/<AgentContext>/{s=1} !s{print} /<\/AgentContext>/{s=0}' <page>.mdx | wc -w # words vs the type's budget411awk '/<AgentContext>/{s=1} !s{print} /<\/AgentContext>/{s=0}' <page>.mdx | grep -o '—' | wc -l # em-dashes ≤ 1 per ~150 words412grep -n '<StatusFlow>' <page>.mdx # bare = gray pills; must carry entity="…"413grep -nE '^(title|description): [^"]*: ' <page>.mdx # unquoted colon-space = YAML, 500s the site414grep -m1 '^description:' <page>.mdx | wc -c # ≤ ~170 raw (the SEO meta: 150–160 chars of text)415awk '/<AgentContext>/{s=1} !s{print} /<\/AgentContext>/{s=0}' <page>.mdx | grep -vE '^\s*([<#|`-]|$)' | awk 'length>480 {print "long paragraph: " substr($0,1,60) "…"}' # split any hit at its idea boundary416```417418Plus two eyeball checks no grep covers: a `## Troubleshooting` heading may exist only inside419`<AgentContext>`, and every `<Figure illustration=…>` key is in the registry list.420421Never declare docs done without: the mechanical gate passing, the new content rendering (in the user's422running dev server, or a clean `pnpm --filter docs build`), every internal link resolving, any new423`<Term>` glossary entries grounded in source + their popovers rendering, names matching real code,424**no generic repeated headings**, and a re-read that confirms each page says what matters / names the425mistake / points onward.426427**Close with a media manifest.** The final report to the user MUST end with the list of images/media the428work needs captured — every unfilled `<Screenshot>` slot on a page you created or touched, one line each:429`page path — slot label (ratio) — what to capture (screen + state)`. Build it with430`grep -n '<Screenshot' <touched pages>`. That list is the user's capture worklist; a slot you don't431report is a slot that never gets filled. No new slots → say "no new media needed" explicitly. Then432record progress (the `.ai/plans/` plan file if one exists, + memory). **Don't kill or rebuild under the user's running dev server.**