@stationhold/content-schema
v0.3.0
Published
Zod schemas for [Stationhold](https://stationhold.com) content: the frontmatter of modules, failure notes, worksites, journal entries and video tutorials, the portable exercise manifest, the extension manifest, and the extension index that the site serves
Readme
@stationhold/content-schema
Zod schemas for Stationhold content: the frontmatter of modules, failure notes, worksites, journal entries and video tutorials, the portable exercise manifest, the extension manifest, and the extension index that the site serves.
It is the contract shared by the Stationhold site, @stationhold/harness-kit and
@stationhold/cli: a manifest that passes these schemas is one the CLI accepts.
Install
npm install @stationhold/content-schemaRequires Node.js 22 or later. The package is an ES module.
Use
Each export is both a Zod schema and the TypeScript type of the same name:
ExerciseManifest, ExtensionManifest, IndexExtensions, ModuleFrontmatter,
PanneFrontmatter, ChantierFrontmatter, JournalFrontmatter,
TutorialFrontmatter, and the smaller pieces they are built from.
import { CHEMIN_INDEX_EXTENSIONS, IndexExtensions } from '@stationhold/content-schema';
const response = await fetch(new URL(CHEMIN_INDEX_EXTENSIONS, 'https://stationhold.com'));
const result = IndexExtensions.safeParse(await response.json());
if (result.success) {
for (const extension of result.data.extensions) {
console.log(extension.id, extension.version, extension.sha256);
}
}Since 0.2.0, an index entry may also carry title, the title of the manifest, and
translations, keyed by language: { "en": { "title": "…", "description": "…" } }.
Both fields are optional, so an index without them still passes. translations accepts
any language key, so a new language on the site does not break an older CLI; the CLI
only reads the languages it knows. LANGUES (fr and en), the Langue type and
estLangue() are exported for the site, @stationhold/harness-kit and
@stationhold/cli.
Since 0.3.0, the package also carries the proof format shared by stationhold verify,
the verify.sh of each kit and the site. encoderPreuve() writes a proof block (plain
text, one fact per line, French keywords: verdicts, the version, the date and the
sha256: fingerprint of the deliverable, never its content), lirePreuve() finds and
reads that block in any pasted text, and normaliserLivrable() is the normalisation
applied before the fingerprint is taken (leading BOM dropped, line endings and trailing
blanks normalised, in linear time; an equivalent awk program is documented next to it).
A deliverable field may now carry label, pattern (on string, date and list),
the choice type with its options, and decimal min/max on number; a deliverable
may declare relations between numeric answers. A pattern is tested as written
(RegExp.test), so it matches anywhere unless anchored with ^…$: [0-9a-f]{7}
accepts zzz1234567zzz, ^[0-9a-f]{7,40}$ does not. validerReponses() checks the YAML
header of a deliverable against its manifest, judging text answers on the text as written
(0123456 keeps its zero), and returns verdicts and reason codes; it never throws. A
scalar answer is read line by line: the text after key:, without the one pair of quotes
around it, with no escape sequence interpreted ("pnpm test -- -t "facture"" reads
pnpm test -- -t "facture"). Lists keep YAML reading, each on its own. An answer that
cannot be read is ABSENT with the reason reponse-illisible and its line; the other
answers are still judged.
The manifests in this repository pass 0.3.0. A manifest written for 0.2.0 may not: 0.3.0
now refuses a check id outside [A-Za-z0-9_-] or used twice, an answer key that is not
lowercase snake case or is reserved (exercice, version_manifeste, date, len),
decimal or negative min/max on list or string, min/max on date, boolean
or choice, and pattern outside string, date and list.
Validation messages are in French, the source language of the content.
License
MIT © 2026 Yeppao. See LICENSE.
