Languages: English · Русский · 简体中文
Assemble multi-language documents from per-section source units that carry every configured language — with drift protection, journalled crash recovery for output writes, and structural translation-parity checks.
Origin: this is a general-purpose extraction of the document
build engine behind ktav-lang/spec —
the same code that assembles the Ktav specification's own spec.md and
its root documents (README, CHANGELOG, CONTRIBUTING, SECURITY) from
per-section source units carrying every language in one file.
Keeping a document's translations in lockstep by hand doesn't survive
contact with a real corpus. ktav-lang/spec shipped a fix for exactly
this shape of defect more than once: a section that keeps every
cross-reference and every keyword while one language quietly loses most
of its bullet points, or a rendered-document parity check that passes
because it only ever looks at the final output, never the sources that
produced it. polydoc is the tooling that grew out of fixing those —
generalized so any project assembling documents from small,
per-section, multi-language units can reuse it instead of reinventing
it.
- Content-unit assembly — a manifest of small unit directories
(
meta.js+body-N.md), each body carrying every configured language behind a>>>>> lang=<code>separator, split at the same paragraph boundary in every language so part k is always the same fragment, never three independent slices that happen to add up. - Root-document assembly — the same unit shape for a handful of hand-maintained-feeling documents (README, CHANGELOG, ...), without the numbered-heading and section-inventory machinery a full spec needs.
- Shared Markdown fragments — define code, images, links or other Markdown once and include it in every translation with reference-count parity.
- Journalled crash recovery for output writes —
writeBuildOutputsjournals a transaction, then backs up and installs outputs sequentially, one file at a time. After a process crash, recovery restores a consistent pre-write state or completes a durable commit. This is not an atomic snapshot for unrelated readers: during backup/install, a reader can temporarily see missing, old, or new files. If a write is interrupted, callrecoverBuildOutputTransactionbefore the next build or validation. File and directory flushing is limited by platform support; Windows and the underlying filesystem/storage can limit durability, so this is not a promise that every acknowledged write survives sudden power loss. - A separate root-document write contract —
writeRootDocswrites each root document directly withwriteFileSync, without this journal or cross-file crash recovery. It provides no cross-file snapshot guarantee; usecheckRootDocs/docs:checkto validate the result afterward. - A docs registry — classifies every public Markdown output as generated (rebuilt and byte-checked), frozen (historical, pinned by a SHA-256 lock) or internal, so a stale frozen file and a brand-new unregistered one are both caught instead of silently shipping.
- Structural translation-parity checks — part alignment, list-item parity, indentation shape, and unwrapped paragraphs, checked against the sources, not the rendered output, where a defect can be pointed at a file and a line.
npm install @ktav-lang/polydocRequires Node.js 24 or later.
configure() must run once, before anything else — it fixes the
language set and file-naming conventions for the rest of the process.
writeBuildOutputs provides journalled process-crash recovery, not an atomic
reader snapshot. checkBuildOutputs throws on byte-level divergence.
import {
configure,
buildBuffers,
writeBuildOutputs,
recoverBuildOutputTransaction,
checkBuildOutputs,
} from '@ktav-lang/polydoc';
configure({
langs: ['en', 'ru', 'zh'],
outFileNames: { en: 'spec.md', ru: 'spec.ru.md', zh: 'spec.zh.md' },
readmeFileNames: { en: 'README.md', ru: 'README.ru.md', zh: 'README.zh.md' },
sectionInventoryLockFormat: 'my-project-section-inventory',
rootDocuments: ['README', 'CHANGELOG'],
});
recoverBuildOutputTransaction('versions/1.0', 'versions/1.0/content');
const build = await buildBuffers('versions/1.0/content', {
requireSectionInventoryLock: true,
sectionInventoryLockPath: 'scripts/locks/section-inventory.1.0.lock.json',
});
writeBuildOutputs('versions/1.0', 'versions/1.0/content', build);
checkBuildOutputs('versions/1.0', 'versions/1.0/content', build);See src/index.mjs for the full exported surface —
content assembly, root documents, the registry, structural checks, and
the transaction internals are all re-exported from the top-level entry
point.
Define language-independent Markdown once, before the first language block:
>>>>> shared=example
```js
console.log(42);
```
>>>>> shared=logo

>>>>> shared=links
[docs]: https://example.com/docs
- Inside every
>>>>> lang=<code>block, put<<<<< include=example,<<<<< include=logoor<<<<< include=linkson its own column-zero line. Translated link labels can use the shared definition:[Read][docs]. - Names are case-sensitive ASCII identifiers: start with a letter, then use
letters, digits,
_or-. Definitions are scoped to onebody-N.mdfile. - Fragments can contain code, images, links, lists, tables or other Markdown. Separator blank lines at the end of definitions are excluded; internal whitespace is preserved. Existing language-separator restrictions still apply.
- Every configured language must use each shared name the same number of times. Empty definitions, unknown names, duplicate names and nested includes fail.
- Includes are not expanded inside fenced code. No files or URLs are loaded, and nothing is executed. Relative links keep their normal Markdown meaning.
Expansion happens before heading, split-size, translation-shape and output checks, and before release-token substitution. Generated Markdown is complete; readers do not need Polydoc or an include extension. Legacy inline sources remain valid.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.