README.md

README.md is a file in Canonical Write. 96 lines of code and 0 definitions.

<!-- Auto-generated 2026-09-29T00:22Z v7 -->

# @govlab/canonical-write

<!-- concern:overview -->

## Purpose

The canonical writers, with one guarantee. `writeCanonicalJson(path, value)` serializes a value and writes it formatted by the project's own prettier configuration. `writeCanonicalText(path, text)` does the same for already-rendered text, inferring the parser from the file extension. The guarantee is that the same input produces the same bytes, on every machine and every run, so a drift check comparing a file against a fresh render sees only semantic differences.

A generated Markdown file also carries one mark, `<!-- Auto-generated <time> v<n> -->`, on its first line or the first line after its frontmatter. `stampGenerated(body, previous, now, normalize?)` compares the new body with the body on disk, both with the mark stripped. An equal body keeps the held mark, so the file stays byte-identical, and a changed body takes the current minute and the next version. `writeGeneratedMarkdown(path, text)` formats first and then stamps against the file on disk.

Every other write goes through `writeVerbatim(path, content)`, which writes text or bytes exactly as given: an edited source file, a cache entry, a built page, a log, a binary, or a generated file whose serializer is already deterministic and which the format stage never reaches. The workspace's write gate bans the raw file-write primitive outside this module, so every write in the workspace passes through one place, and a write helper elsewhere cannot hide a write from the gate.
<!-- /concern:overview -->

<!-- concern:use -->

## When to use

- Any generator whose output is committed and drift-checked, such as a catalog, an index, a generated README or a report artifact.
- Writing JSON that the developer will read in a diff, where a formatting-only change would bury the semantic one.
- Emitting rendered text (markdown, TypeScript) that must match a fresh render byte for byte.
- Writing a cache entry, a log, a built page, an edited source file or a binary through `writeVerbatim`, whose bytes are the caller's.

## When NOT to use

- A test fixture under a temporary directory, which the write gate leaves to the raw primitive.
- A hot loop formatting thousands of small files through a canonical writer. Formatting is not free, so a generated artifact the format stage never reaches goes through `writeVerbatim`.

<!-- /concern:use -->

<!-- concern:charts -->

## Architecture charts

The structure, logical-flow and dependency diagrams derived from the source AST live in [_code.info.generated/mermaid-charts.generated.md](./_code.info.generated/mermaid-charts.generated.md).
<!-- /concern:charts -->

<!-- concern:install -->

## Install

A private workspace package resolved through the root `package.json` workspaces glob `govlab.root/govlab.utils/*`. One `npm install` at the repo root links it, and `prettier` is hoisted there instead of declared here. There is no build step, so a consumer imports `@govlab/canonical-write` and calls it.

## Quick start

```ts EXAMPLE: Write a generated artifact whose bytes must be reproducible
import { writeCanonicalJson, writeCanonicalText } from "@govlab/canonical-write";

await writeCanonicalJson(resolve(outDir, "catalog.generated.json"), { rules, summary });
await writeCanonicalText(resolve(outDir, "INDEX.generated.md"), renderedMarkdown);
```

```ts EXAMPLE: Drift-check a written artifact against a fresh render
await writeCanonicalJson(path, value);

const fresh = await renderCanonical(value);
if (readFileSync(path, "utf8") !== fresh) {
    throw new Error(`${path} has drifted — regenerate it`);
}
```

```ts EXAMPLE: Write a generated Markdown document whose mark moves only when its body changes
import { writeGeneratedMarkdown } from "@govlab/canonical-write";

await writeGeneratedMarkdown(resolve(outDir, "INDEX.generated.md"), renderedMarkdown);
```

<!-- /concern:install -->

<!-- concern:api -->

## API

- `function composeMark(mark: GeneratedMark): string`
- `const GENERATED_MARK_PREFIX: "<!-- Auto-generated "`
- `interface GeneratedMark`
- `function markTimeOf(date: Date): string`
- `function parseMark(text: string): GeneratedMark | null` — reads the time and version from a mark line, and answers null for a text with no mark or a malformed one. `stripMark` removes the line and the blank lines after it.
- `function stampGenerated(body: string, previous: string, now: Date, normalize?: (text: string) => string): string` — returns the body with its mark: the held mark when the normalized body is unchanged, otherwise the current minute and the next version. A drift spec's produce step calls it with the file on disk.
- `function stripMark(text: string): string`
- `function writeCanonicalJson(target: string, data: unknown, options?: Options): Promise<void>` — serializes a value and writes it formatted by prettier with the caller's options. It is the writer for every generated `.json` artifact.
- `function writeCanonicalText(target: string, text: string, options?: Options): Promise<void>` — writes already-rendered text through the same formatter, inferring the parser from the file extension, for generated markdown and TypeScript.
- `function writeGeneratedMarkdown(target: string, text: string, options?: Options): Promise<void>` — formats the text, then stamps it against the file on disk and writes it. It is the writer for a generated Markdown file that no drift check produces.
- `function writeVerbatim(target: string, content: Uint8Array | string): void` — writes text or bytes exactly as given. It is the writer for every file whose bytes the caller already owns.

<!-- /concern:api -->

<!-- concern:config -->

## Configuration

None of its own. Each writer formats with the prettier options the caller passes, and infers the parser from the target path's extension. A caller whose output the format stage also reaches passes the same options that stage uses, so the stage leaves the file as written.
<!-- /concern:config -->

<!-- concern:deps -->

## Dependencies

The package is a leaf with no runtime dependencies.
<!-- /concern:deps -->

<!-- concern:ai-context -->

## AI context

- Canonical means reproducible, not merely pretty. A drift check comparing a file against a fresh render sees only semantic differences.
- Every writer is async. A generator that forgets to await writes nothing and still exits zero.
- A generator stamps after it formats. Stamping unformatted text compares a body the format pass then rewrites, so the version rises on every run.
- A drift check compares the stamped file with the mark kept, so a hand edit to the mark counts as drift.
- Formatting uses the options the caller passes, not a config file on disk. Raw bytes get rewritten by the next format pass, and a drift gate reports that rewrite.
- A pure leaf: zero `@govlab/*` sibling dependencies, so the packages that generate artifacts depend on it without a cycle.

<!-- /concern:ai-context -->

<!-- concern:domains -->

## Domains

This package serves these software domains, which `_manifest.json` declares in `domains` from the two-tier software-domain vocabulary (`meta → sub`):

- **developer-tooling** — code-generation
- **platform** — file-system

<!-- /concern:domains -->

<!-- concern:quality-governance -->

## Quality governance

The canonical quality catalog resolves the quality concepts that govern this package. `_manifest.json` declares them in `governedBy`, and a lint package derives them from the concepts its own rules enforce. Each maps to the custom lint rules that enforce it:

- **immutability** — _best-practice_
- **serialization** — _best-practice_

<!-- /concern:quality-governance -->

<!-- concern:disposal -->

## Disposal

- Declare another module as the write owner in the write gate's options, then move each call site to it. Generated bytes become writer-dependent unless the new owner formats them.
- Weaken or delete every `--check` drift gate that compares a generated file against a fresh render.
- Drop `@govlab/canonical-write` from the `govlab.quality` and `govlab.pipeline` manifests, then remove the package and its workspace entry.

<!-- /concern:disposal -->

<!-- concern:metrics -->

---

stable · 11 exports · 0 deps · 0 principles · 2 concepts
<!-- /concern:metrics -->