Skip to content
Tsumugu

Diagnostics#

Why diagnostics are values#

A documentation tool meets recoverable problems constantly: unparsable front matter, a construct it cannot represent, a route two files both want, a link to a page that does not exist.

Writing those to the console makes them impossible to test, aggregate, sort or render into a page. So every stage returns diagnostics, and nothing in the pipeline logs. The CLI formats them as text; a theme renders the same fields as HTML. Neither presentation is mentioned in the model.

Diagnostics are not a replacement for assertions. A broken internal invariant is a bug in Tsumugu and should throw; a diagnostic describes something about the user's project.

Severity#

Severity is measured by blast radius, not by how annoying the problem is.

SeverityMeaningConsequence
warningthe document is still usableit is served, with the problem reported
errorthis document cannot be producedthe rest of the project still can; the document survives as a record so the failure can be explained on its own page
fatalnothing can be producedthe process reports the error and stops

There is exactly one fatal condition today: the documentation root cannot be read. Everything else is scoped to a document or narrower.

Shape#

FieldRequiredPurpose
codeyesstable identifier, stage/kebab-case
severityyeswarning, error, fatal
messageyeswhat went wrong, in a sentence an author can act on
hintnowhat to do about it, when there is a concrete answer
sourcePathnowhich file
rangenowhere in that file, when a parser reported a position
stagenowhich stage produced it
causenothe underlying thrown error
relatednoother locations that help explain it

Match on code, never on message. Messages are for humans and may be reworded or eventually translated; codes are the contract.

hint is separate from message so a presentation can show it differently, and so messages are not padded with advice when there is none to give.

cause is kept so a stack trace stays reachable while debugging. It is deliberately not part of a diagnostic's identity. Two reports of one problem are one problem, whichever exception object produced them.

Ordering and deduplication#

sortDiagnostics orders worst first, then by file, then by position within the file, then by code and message.

Stages may run concurrently, so arrival order depends on scheduling. Sorting means the same project always produces the same list, which is what makes the output diffable and the tests meaningful. Position ordering serves the workflow that follows "what is wrong": working down a file fixing things.

dedupeDiagnostics removes repeats. The same underlying problem is often noticed by more than one stage. A file that cannot be parsed fails to render and then fails to route. Identity excludes cause, hint and related, because those explain a problem rather than distinguish one.

Codes#

Every code in the implementation appears here. tests/diagnostic-codes.test.ts fails if one does not.

document/#

CodeSeverityWhen
document/invalid-source-patherrora path is absolute or escapes the documentation root
document/unsupported-formatwarningno renderer handles the extension; images and licences hit this constantly, so it is not reported by the scanner

scanner/#

CodeSeverityWhen
scanner/root-unreadablefatalthe documentation root does not exist or cannot be read
scanner/unreadablewarninga subdirectory cannot be read; its contents are missing from the scan and everything else still works
scanner/symlink-skippedwarninga symbolic link was not followed, because one can point outside the root

cache/#

CodeSeverityWhen
cache/unreadableerrora file could not be read for a reason other than being gone; the last good version is kept

routing/#

CodeSeverityWhen
routing/collisionerrortwo or more files map to the same route
routing/unroutableerrora source path could not be mapped to a route and is not served
CodeSeverityWhen
navigation/duplicate-labelwarningtwo entries in the same section read identically; both pages still work and are linked

build/#

CodeSeverityWhen
build/missing-originwarningno origin was given, so sitemap.xml was written with a placeholder
build/collisionerrortwo outputs want the same file; the first is written and the second is named
CodeSeverityWhen
link/unknown-documentwarninga link points at a route the project does not serve
link/unknown-fragmentwarninga link points at a heading identifier the target document does not have
link/missing-assetwarninga link points at a file that is not in the documentation root

metadata/#

CodeSeverityWhen
metadata/invalid-titlewarningtitle is not text; the next fallback is used
metadata/invalid-descriptionwarningdescription is not text; it is omitted
metadata/invalid-orderwarningorder is not a finite number; the page is ordered as if it had none
metadata/invalid-hiddenwarninghidden is not a boolean; the page stays visible
metadata/unknown-key-typowarningan unknown key is one letter from a known one; genuinely unknown keys stay silent

renderer/#

CodeSeverityWhen
renderer/noneerrorno registered renderer claims the document
renderer/ambiguouserrormore than one renderer claims it
renderer/duplicate-iderrortwo renderers share an id
renderer/threwerrora renderer threw; the original is kept as cause

transformer/#

CodeSeverityWhen
transformer/duplicate-iderrortwo transformers share an id; only the first runs
transformer/threwerrora transformer threw; the document keeps the shape it had before that transformer
transformer/invalid-resulterrora transformer returned something other than a document; its change is discarded
transformer/invalid-heading-idwarningan identifier written in the source cannot be a URL fragment; one is derived from the heading
transformer/duplicate-heading-idwarningtwo headings claim one identifier; the later one is suffixed so both stay reachable

serializer/#

CodeSeverityWhen
serializer/invalid-nodeerrora virtual node could not be serialized safely; it is skipped rather than emitted as broken markup

theme/#

CodeSeverityWhen
theme/missing-rendererwarningthe theme has no renderer for a node type; its content is shown without presentation
theme/renderer-threwerrora node renderer threw; that node loses its presentation, the rest of the page survives
theme/unsupported-nodewarninga renderer could not represent some source; it is shown as preformatted text

renderer-markdown/#

CodeSeverityWhen
renderer-markdown/unsupported-constructwarningMarkdown the Semantic AST cannot represent yet; the source is kept and shown
renderer-markdown/invalid-front-matterwarningthe front matter is not valid YAML, or is not a mapping; the document still renders
renderer-markdown/unsupported-metadata-valuewarninga front-matter value has no metadata representation, such as a date or nested map
renderer-markdown/split-scriptwarningan inline script is split across Markdown nodes, so it cannot be hashed or run

transformer-highlight/#

CodeSeverityWhen
transformer-highlight/unknown-languagewarningno grammar exists for the language on the fence; the code is shown plain
transformer-highlight/failedwarninga grammar failed while tokenizing; the code is shown plain

transformer-mermaid/#

CodeSeverityWhen
transformer-mermaid/not-drawnwarningthe diagram is outside the subset Tsumugu draws, or does not parse; it is shown as code instead (ADR 9)

The message names the construct and the position points inside the diagram, not at the fence, so an author looking for the problem looks in the right place.

renderer-openapi/#

CodeSeverityWhen
renderer-openapi/unparsablewarningthe file is not valid YAML or JSON; the page says so and names the parser's message
renderer-openapi/incompletewarningthe description was read but something in it was not: an unresolvable reference, a reference into another file, or a version Tsumugu does not read (ADR 10)

renderer-mdx/#

Produced only by the opt-in executing MDX renderer, which a composition registers under the operator's --trust declaration (ADR 7).

CodeSeverityMeaning
renderer-mdx/execution-failedwarningthe document would not compile or threw; it renders without execution instead
renderer-mdx/inline-scriptwarningMDX reads a script's content as content, so an inline script cannot be run

renderer-html/#

CodeSeverityWhen
renderer-html/script-removedwarningscript content was removed; documentation JavaScript is disabled by default
renderer-html/unsupported-elementwarningan element has no semantic equivalent; its markup is preserved as untrusted content

Formatting#

formatDiagnostic produces plain text in the compiler convention that editors and humans already read:

docs/guide.md:12:3: error routing/collision: "docs/guide.md" maps to "/guide", and so do 1 other file(s).
  hint: Rename or move one of them. Which page is served would otherwise depend on the order the files happened to be scanned.
  see also: docs/guide/index.md: also maps to "/guide"

No colour, no symbols, no escape codes. Those belong to whatever is displaying the diagnostic; baking them in would make the same function useless in a browser, a log file or a test assertion.

Adding a code#

  1. Name it stage/kebab-case, describing the problem rather than the fix.

  2. Pick severity by blast radius, using the table above.

  3. Write a message an author can act on, and a hint only if there is a concrete answer.

  4. Attach sourcePath, range and stage when they are known.

  5. Preserve cause when converting a thrown error.

  6. Add it to this document. A test fails if the index is missing a code.