The structural checks on an embedded manual, as functions that return a
list of failure messages (empty when the manual is sound).
Managoat.Docs.GuardrailCase wraps each one as an ExUnit test; they are
functions first so a host can run one from a script, a release task or a
test of its own shape.
Each check exists because of an incident. The messages say which, and what
to do; they are kept verbatim from Fountain's docs_test.exs because they
are the documentation of the incident and better than a summary of it.
Every function takes the host's docs module (the one that uses
Managoat.Docs) and reads the manual through its generated functions, so
a check runs against what the module embedded, not a re-read of the
files.
Summary
Functions
Every internal #anchor link targets a heading id on the rendered target
page. The rendered HTML is the ground truth: to_trusted_html/1 gives
every heading a GFM-style id, and comrak's slug is the only one that
matters. (The MkDocs build that slugged the same headings differently for
duplicate headings, comrak -1 against python-markdown _1, is the
reason no second slugging pass is allowed.)
The source of every plain COPY src dst line in a Dockerfile (flags such as --from excluded).
Every file the docs module reads at compile time (external_resources/0)
exists and is COPYd into the image's build stage.
{file, info} for every fence in the given markdown files.
Every fenced code block in the manual (compiled bodies, so snippet
includes count) names a language whose parser languages/0 bakes, or one
in unhighlightable. A fence in a language that is not baked renders
unhighlighted in production and nowhere else, which is how it goes
unnoticed (Fountain #879).
Every internal link under the mount targets a page that exists.
Every name in languages/0 is a Lumis language id, so the Dockerfile's
Lumis.Languages.cache/1 step succeeds.
Every page the nav names exists on disk.
No authoring syntax survives preprocessing: every relative .md link was
rewritten to a path under the mount, every admonition became a blockquote,
every snippet include was expanded. A page that introduces syntax the
compiler does not rewrite is caught here.
Every .md page on disk under the docs directory is named in the nav.
Every slug resolves to a page with a title and a non-empty body, and the home page is at "".
The search index has exactly one entry per page.
Every heading id in the search index resolves on its own rendered page,
and no heading has empty text. Same ground truth as anchors_resolve/1.
search_index_json/0 round-trips through JSON to the same number of
entries as search_index/0, and contains no </ that could close the
<script> tag a layout inlines it into.
{page, path} for every snippet include in the manual's source pages.
Every --8<-- "path" snippet include in the manual points at a file that
exists and that the Dockerfile COPYs into the build stage.
fences_baked/2 over an explicit {where, info} list, for a host that has
authored markdown outside the manual (an in-app help directory, say).
fenced_languages/1 scans files into that shape.
Types
@type failure() :: String.t()
A failure message. An empty list is a pass.
Functions
Every internal #anchor link targets a heading id on the rendered target
page. The rendered HTML is the ground truth: to_trusted_html/1 gives
every heading a GFM-style id, and comrak's slug is the only one that
matters. (The MkDocs build that slugged the same headings differently for
duplicate headings, comrak -1 against python-markdown _1, is the
reason no second slugging pass is allowed.)
The source of every plain COPY src dst line in a Dockerfile (flags such as --from excluded).
Every file the docs module reads at compile time (external_resources/0)
exists and is COPYd into the image's build stage.
The generalisation of snippets_copied/2. Snippets are found by scanning
markdown; this asks the module itself what it read, so a new kind of
compile-time dependency is covered without anyone remembering to extend a
scanner. The nav file is the case that motivated it: it is parsed at
compile time, and nothing about a snippet scan would ever have noticed it
was missing from the Dockerfile.
{file, info} for every fence in the given markdown files.
Every fenced code block in the manual (compiled bodies, so snippet
includes count) names a language whose parser languages/0 bakes, or one
in unhighlightable. A fence in a language that is not baked renders
unhighlighted in production and nowhere else, which is how it goes
unnoticed (Fountain #879).
unhighlightable names the fences exempt from the rule: the ones that ask
for no highlighting (text, plain, ...) and the ones Lumis has no
parser for. Adding to it means checking that Lumis really has no parser,
not that baking one is inconvenient.
Every internal link under the mount targets a page that exists.
Every name in languages/0 is a Lumis language id, so the Dockerfile's
Lumis.Languages.cache/1 step succeeds.
No authoring syntax survives preprocessing: every relative .md link was
rewritten to a path under the mount, every admonition became a blockquote,
every snippet include was expanded. A page that introduces syntax the
compiler does not rewrite is caught here.
Every .md page on disk under the docs directory is named in the nav.
The other direction, and the one Fountain had no gate for while a static
site build still existed. MkDocs built every page under docs/ whether
the nav named it or not, so a page left out of the nav was still reachable
on the published site and its absence from the in-app manual looked like a
rendering quirk rather than a mistake. Four such pages had accumulated.
With one publisher, a page missing from the nav is a page that is published nowhere at all: written, merged, and invisible. There is no allowlist on purpose. Somewhere else in the repository is the right home for a markdown file nobody should read here.
Every slug resolves to a page with a title and a non-empty body, and the home page is at "".
The search index has exactly one entry per page.
Every heading id in the search index resolves on its own rendered page,
and no heading has empty text. Same ground truth as anchors_resolve/1.
search_index_json/0 round-trips through JSON to the same number of
entries as search_index/0, and contains no </ that could close the
<script> tag a layout inlines it into.
{page, path} for every snippet include in the manual's source pages.
Every --8<-- "path" snippet include in the manual points at a file that
exists and that the Dockerfile COPYs into the build stage.
The manual inlines snippets at compile time, reading from root. Inside
the image that root is whatever the Dockerfile COPYed, so a snippet
pointing outside it does not degrade: mix release dies on File.read!
and no image is produced at all. That is a silent failure in the worst
place: CI is green, the PR merges, and the deploy simply never happens. It
has happened once (Fountain's docs/tour.md including an SDK example).
fences_baked/2 over an explicit {where, info} list, for a host that has
authored markdown outside the manual (an in-app help directory, say).
fenced_languages/1 scans files into that shape.