wiki-knowledge

The wiki title lives in a vault config file and resolves in one place: flag → config → directory name

Context. The static export gave every page a sticky nav bar and a front page heading carrying a wiki title, defaulted to the vault root directory name (ADR-0022) — the one thing a vault calls itself without being told. That default is a filesystem accident: the live dogfooding vault is a directory called enchiridion-vault, and a recipient opening an emailed export reads whatever the directory happened to be named rather than what the wiki is about. #477 makes the title settable — persistently, and overridden for a single run.

Decision 1 — the saved title lives in .wiki-knowledge/config.json, beside the search index. The export title is deployment-local presentation state, not knowledge: it names the wiki to a reader of one exported copy, and it is not something a page should link to, be typed against, tagged with, or be indexed by. Putting it in the vault as content (a committed page, or a frontmatter key on one) would make it all of those things and give it a merge conflict every time the synced vault is edited in two places at once. So it goes where the plugin’s other local state already goes — .wiki-knowledge/, beside index.db (ADR-0006) and the ingest/watch locks — a directory enchiridion init already gitignores, and which ADR-0006 already requires be excluded from Resilio Sync. The saved start page (ADR-0022, #567) is the same kind of value for the same reason: a nomination about the export, not a property of the page, so nothing in the page records it and two clones can render the same vault with different front doors. The schema today is {"title": string, "startPage": string}, both optional, and unknown keys survive a save: the save helpers read, merge, write, so a future key is not silently dropped by a code path that only knows about these two.

Decision 2 — a missing or broken config is “nothing configured”, never an error. The file is absent until something writes it; readExportConfig returns an empty config for an absent file, an unreadable one, malformed JSON, a non-object, and a non-string title or startPage alike. Each of those means the same thing to every caller — nothing was configured — and a title that fails to apply is a cosmetic miss with an obvious next step (run the save again), not a condition worth failing an export over. Degrading to the directory name is also the safe direction: for any vault that has a name to fall back to, the resolved title is a real string, and the one root that has none (a filesystem root, whose basename is empty) still lands on the render layer’s neutral label rather than an empty nav bar — the last-resort default #476 already established, not a second resolution. The saved start page is the one stated exception, and it is not a broken-config case: a well-formed ref naming a page the export does not carry fails the export loudly, naming .wiki-knowledge/config.json, rather than degrading to the generated front page. Silently reverting to the generated page is exactly the bug the setting exists to prevent, and a save that stopped matching (a page moved, or --raw dropped) would otherwise hide behind a site that still looks finished. An absent, malformed or wrong-typed config still degrades to “no start page”; only a readable ref that the run cannot honour is an error.

Decision 3 — the resolution order lives in exactly one function. resolveExportTitle(root, flagTitle) in enchiridion-ts/src/exportconfig.ts returns the first of: the per-run flag, the saved title, the vault root’s directory name. The writer calls it once (ADR-0022’s “derived in exactly one place” now means this function, which is also the only thing that reads a vault to name one); renderers receive the resolved string and never inspect a vault, and a render pass invoked with no vault at all keeps the neutral "Wiki" label from #476. A blank value at either of the first two levels means “not supplied” and falls through — an empty --title cannot blank out the nav bar. The start page resolves beside it in resolveExportStartPage(root, flagStartPage) — flag → saved startPage → none — with the same blank-falls-through rule, and returns the ref already normalised (leading ./ stripped, .md appended when absent) so a typed flag and a hand-edited config reach the export in one spelling. Neither function validates the value against the vault: a title needs no validation, and the start page’s two questions (is it a page of the vault? does this run’s exported set carry it?) belong to the save path and the export path respectively, each of which has the enumeration the other lacks.

Decision 4 — persistence is a subcommand, not skill-authored JSON. enchiridion export --save-title <title> writes the config and exits, writing no site — persist-and-exit in the shape --candidates already uses. --save-start-page <ref> is the same shape, with one deliberate asymmetry: a blank ref clears the key rather than being refused, because “no start page” is a state an operator returns to while “no title” is not. It validates the ref against the vault’s page enumeration (every wiki/ page plus every file under raw/) before writing, but not against the exported set — that depends on --raw and is the export’s own check. The /wiki-export skill therefore shells out to save rather than hand-writing JSON, which keeps the file’s format, location, and merge behaviour inside the tested helpers, and matches the standing convention that deterministic work is a subcommand (a skill that invented the JSON would be a second, untested writer of the same file). A second full export is deliberately not what “save” means here: the skill saves after it has already exported, over a target that is now non-empty. A blank title is refused rather than written — “no title” is spelled by not having one, and a stored blank would read back as unset anyway; that is the asymmetry with the start page’s blank-clears rule above.

Mechanism

Consequences

What would reopen this

The config growing something the vault itself must see — a value pages link to, or that ingestion must honour. That is content, and content belongs in the vault and in git; the split this ADR draws is presentation-local versus knowledge, and a key that crosses it reopens the location decision rather than merely extending the file.