Metadata-Version: 2.4
Name: nide
Version: 2026.8.16
Summary: A bound volume (Finnish: nide) - assemble markdown text files into gfm-plus, html, and pdf channel outputs.
Author-email: Stefan Hagen <stefan@hagen.link>
Maintainer-email: Stefan Hagen <stefan@hagen.link>
License-Expression: MIT
Project-URL: Documentation, https://codes.dilettant.life/docs/nide
Keywords: developer-tools,documentation,markdown
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: defusedxml>=0.7.1
Requires-Dist: jsonschema>=4.26.0
Requires-Dist: kaava>=2026.6.20
Requires-Dist: python-jsonpath>=2.0.2
Requires-Dist: PyYAML>=6.0.3
Requires-Dist: vyyhti>=2026.6.21
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Provides-Extra: manifest
Requires-Dist: blake3; extra == "manifest"
Requires-Dist: pyhanko; extra == "manifest"
Requires-Dist: tallipoika; extra == "manifest"
Dynamic: license-file

# nide

A bound volume (Finnish: nide) - assemble markdown text files into gfm-plus, html, pdf, and typst channel outputs.

Requires Python 3.11 or later.

## Install

Install the core package:

```
pip install nide
```

To record and verify source-tree provenance with `nide manifest` and `nide validate`,
install the optional manifest dependency group:

```
pip install nide[manifest]
```

This adds three packages:

| Package      | Purpose                                                                                       |
|:-------------|:----------------------------------------------------------------------------------------------|
| `tallipoika` | RFC 8785 canonical JSON - required for `nide manifest` generation                             |
| `blake3`     | BLAKE3 hashing - sha256-only fallback if absent; both hashes recorded when present            |
| `pyHanko`    | PDF XMP injection - used by `nide manifest --inject-pdf` to write Dublin Core metadata as XMP |

`nide manifest --summary` and `nide validate` read an existing `build/manifest.json`
and do **not** require `tallipoika`.  They can run in environments where only the core
package is installed, provided the manifest was generated elsewhere.

## Manual

The [man page](docs/man/nide.1) provides the CLI reference (`man nide` after placing the file on your `MANPATH`):

```sh
mkdir -p ~/.local/share/man/man1
cp docs/man/nide.1 ~/.local/share/man/man1/
```

## Quickstart

Run `nide` with no arguments for a usage summary, or `nide --version` to check the installed version.
For a dedicated feature walkthrough you can follow in minutes visit [quickstart](docs/quickstart/README.md).
A step-by-step guided build of a document pipeline is provided in the [tutorial](docs/tutorial/README.md).
Some commented real-world configurations are provided in the [example](example/README.md) documentation.

## Pipeline

Two-pass workflow:

```
nide harvest              ->  build/section-*.json, build/example-*.json

nide assemble             ->  build/ir.json          (Pass 1: channel-neutral IR)
nide render -t html       ->  build/html.md, build/section-records.json
pandoc                    ->  build/pandoc.html
nide inject               ->  build/injected.html
tidy                      ->  delivery HTML

nide render -t gfm-plus   ->  build/gfm-plus.md

nide render -t typst      ->  build/typst.md, build/nide-columns.lua
pandoc -t typst           ->  build/typst.typ
typst compile             ->  delivery PDF           (recommended PDF path)

nide render -t pdf        ->  build/pdf.md           (legacy: liitos/LaTeX path)
liitos                    ->  delivery PDF

nide manifest             ->  build/manifest.json + embedded blocks in channel outputs
nide validate             ->  six-check integrity report (zero exit on pass)

nide quality --rules etc/rules/spec.rules.yaml  ->  per-rule PASS/FAIL/NA report
```

The compat one-pass form combines Pass 1 + Pass 2 into a single command
(`nide assemble -t html|gfm-plus|pdf`) for existing workflows.

## Commands

`nide` has thirteen subcommands.  Every name accepts any unambiguous prefix -
`nide a` = assemble, `nide r` = render, `nide m` = manifest, `nide val` = validate, `nide ej` = eject, `nide ex` = explain, `nide q` = quality, etc.

**`nide assemble`** (Pass 1) splices a list of Markdown source files into a single
channel-neutral intermediate representation (IR) and writes it to `build/ir.json`:

- Citation definition files are expanded into formatted inline citation entries.
- Glossary content is captured per source file for channel-specific rendering.
- Section headings are numbered and stored with all metadata needed by any channel.
- Cross-references (`[sec](#label)`, `[cite](#label)`, `[tab](#label)`,
  `[fig](#label)`, `[eg](#label)`) are resolved from JSON look-up tables produced
  by `nide harvest`.
- Standalone local image references (`![alt](path)` as the entire line) are
  resolved to absolute paths relative to their source file's directory and stored
  as `ImageNode` entries in the IR.  Remote URLs pass through verbatim.

When called with `-t CHANNEL`, `nide assemble` also runs Pass 2 immediately
(compat shortcut).

**`nide render`** (Pass 2) reads `build/ir.json` and renders one or more channels.
Accepts a comma-separated list: `nide render -t gfm-plus,html,typst,pdf`.

Before writing any channel output, `nide render` copies all locally-resolved images
to `build/images/` and rewrites image references in all channels.  No Makefile `cp`
step is required - the image tree is managed automatically.  A bundled `missing.svg`
placeholder is installed when any image could not be found at assembly time.

- `html`: numbers headings with inline anchors; expands glossary into `<dl>` elements;
  writes `build/html.md` and `build/section-records.json`.
- `gfm-plus`: numbers headings with inline anchors; passes glossary through verbatim;
  writes `build/gfm-plus.md`.
- `typst`: converts headings, code blocks, anchors, and cross-references to raw typst
  blocks; writes `build/typst.md` and `build/nide-columns.lua`.
  Recommended path for PDF output: `pandoc -t typst … | typst compile`.
- `pdf` *(deprecated)*: attaches the nide-generated pandoc heading attribute `{#label}` to every
  heading so hyperref destination names match the cross-reference LUT; prepends
  `\phantomsection` before unnumbered (annex/appendix) section headings; replaces
  the remote logo URL with the local path; writes `build/pdf.md`.
  Use the typst channel instead for new workflows.

**`nide inject`** post-processes the HTML file that pandoc has produced from
nide's `build/html.md`, applying:

- Custom HTML table of contents (built from `build/section-records.json`).
- `<title>` element replaced with the value of `html-title` from config.
- Base and skin CSS inlined; `<link>` tags removed.
- Remote logo URL replaced with a local data URL.
- Image `src` attributes optionally replaced with base64 data URLs.

The result is written to `build/injected.html`.

**`nide harvest`** scans the Markdown sources and writes JSON look-up table (LUT)
files for section and example cross-references.  Run it before `nide assemble`
whenever source files change.

**`nide explain`** performs a dry-run walkthrough without writing any output files.
It prints config values, binder contents with exist/missing tags, channel exclusions,
auxiliary source file status, and LUT file presence.  Useful for diagnosing why an
assembly produces unexpected output.

**`nide doctor [--fix]`** scans the project layout and reports findings at ERROR or
WARN level: missing directories, binder file, source files, first-authored-section
heading, auxiliary sources, and LUT files.  With `--fix`, it creates missing
directories and runs `nide harvest` to generate absent LUT files.

**`nide eject`** emits a fully-commented `nide.yaml` template to stdout.
Use `-o PATH` to write it to a file; add `-f` to overwrite an existing file.

**`nide manifest [-c PATH]`** records source-tree provenance after all channel renders are
complete.  It hashes every binder source file, the config file, and all channel output files,
detects the active CCS (git, fossil, or none), records branch/revision/per-file dates,
captures toolchain versions, and writes `build/manifest.json`.  It also appends a canonical
JSON block to each channel output file (HTML comment for GFM-plus and HTML; `/* … */` for
typst); the operation is idempotent.  Requires `pip install nide[manifest]` for the
`tallipoika` canonical-JSON dependency.

**`nide manifest --summary [--format text|markdown|yaml]`** reads an existing
`build/manifest.json` and prints a human-readable rendering to stdout.  Does not require
tallipoika.  Default format is `text` (aligned); `markdown` produces pipe tables; `yaml`
produces a PyYAML human-readable subset.

**`nide validate [--strict] [--quiet]`** reads `build/manifest.json` and runs six integrity
checks: manifest self-hash, per-source-file content hashes, config hash, binder composition
hash, per-channel output hashes (embedded blocks stripped before hashing), and cross-channel
source-state consistency.  Exits 0 (all pass), 1 (any fail), or 2 (usage error / manifest
not found).  `--strict` treats WARN as failure; `--quiet` suppresses per-check output.

**`nide quality [--rules PATH[,PATH…]] [--strict] [--json] [--quiet]`** reads
`build/ir.json` and evaluates each rule in the configured YAML rules files,
reporting `PASS`, `FAIL`, or `NA` per rule.
Supports eight check types: `section-present`, `section-absent`, `metadata-present`,
`all-match` (with optional `invert: true`), `any-match`, `count`, `references-resolve`,
and `conditional` (condition failing maps to `NA`, condition passing will evaluate the `then` check).
Checks operate on IR targets: `prose`, `headings`, `links`, `relative-links`, `images`,
or `section:<pattern>`.
Rules files use a two-file convention: an org-level base file (fetched from
publication-assurance, do not edit) and a spec-level file that extends it with
`meta.extends: <name>` and adds spec-specific rules.
Rules files are specified with `--rules` or under `quality.rules` in `nide.yaml`.
Exit codes: 0 (no BLOCKER failed), 1 (at least one BLOCKER failed), 2 (usage error).

**`nide version`** (also `-V` / `--version`) prints the installed version and exits.

**`nide completion bash|fish|zsh`** prints a shell completion script to stdout.
Requires `pip install shtab`.

Four output channels are supported:

| Channel    | Output (`nide render`)                              | Intended consumer       |
|:-----------|:----------------------------------------------------|:------------------------|
| `html`     | `build/html.md` + `build/section-records.json`      | pandoc to `nide inject` |
| `gfm-plus` | `build/gfm-plus.md`                                 | direct delivery         |
| `typst`    | `build/typst.md` + `build/nide-columns.lua`         | pandoc to typst compile |
| `pdf`      | `build/pdf.md` *(deprecated - use `typst` instead)* | `liitos`                |

IR output (`nide assemble`): `build/ir.json`.

## Configuration

All settings are read from `etc/nide.yaml` (override with `-c`).
Every key is optional; the built-in defaults are listed below.
Run `nide eject` to emit a fully-commented template with every key.

Settings are grouped under seven top-level YAML keys.

### `assembly:` - source file handling

```yaml
assembly:
  binder: bind.txt                      # binder filename (in etc-path)
  source-path: src                      # directory containing source .md files
  build-path: build                     # output directory
  etc-path: etc                         # directory holding this config and LUT files
  first-authored-section: '# Scope'    # heading where body numbering starts
  binder-ignores:
    gfm-plus: []                        # filenames to skip for GFM-plus channel
    html: []                            # filenames to skip for HTML channel
    typst: []                           # filenames to skip for typst channel
    pdf: []                             # filenames to skip for PDF channel (deprecated)
  delete-when:
    - contains: '<mark title="Ephemeral region marking">'
      delete:
        - '<mark title="Ephemeral region marking">'
        - '</mark>'
  output-paths:
    ir: build/ir.json
    gfm-plus: build/gfm-plus.md
    html: build/html.md
    typst: build/typst.md
    pdf: build/pdf.md
```

### `structure:` - document anatomy

```yaml
structure:
  section-type-prefixes: [Annex, Appendix]   # prefixes that suppress auto-numbering
```

Headings whose text starts with one of these prefixes (plus a space) are treated
as special sections.  The letter designator is spelled out in the source file.

### `enumeration:` - counters and look-up tables

```yaml
enumeration:
  sections:
    l1-dot: true               # trailing dot on level-1 labels (1., 2., …)
    sub-dot: false             # trailing dot on level 2–6 labels
    reference-style: number    # [sec] expansion: number | number-title | section-sign-number
    toc-template:              # 5 entries, one per heading level
      - '$sec_cnt_disp$ [$text$](#$label$)  '
      - "\t$sec_cnt_disp$ [$text$](#$label$)  "
      - "\t\t$sec_cnt_disp$ [$text$](#$label$)  "
      - "\t\t\t$sec_cnt_disp$ [$text$](#$label$)  "
      - "\t\t\t\t$sec_cnt_disp$ [$text$](#$label$)  "
    lut:
      label-to-display: section-label-to-display.json
      display-to-label: section-display-to-label.json
      display-to-text:  section-display-to-text.json
  examples:
    track: false               # enable *Example N: scanning and look-up tables
    global-start: 4321         # global counter offset (must exceed local count)
    lut:
      local-to-global: example-local-to-global.json
      global-to-local: example-global-to-local.json
```

When `l1-dot: true` (default), nide injects `\renewcommand{\thesection}` into
the PDF channel output so LaTeX section numbers match nide's numbering.

### `references:` - cross-reference configuration

```yaml
references:
  citations:
    sources: []                                           # citation source filenames
    skip-prefixes: ['#']                                  # lines bypassing the state machine
    keyword: cite                                         # inline trigger: [cite](#label)
    cite-template: "\\[[$code$](#$label$)\\]"            # inline ref expansion template
    entry-template: '**\[**<span id="$label$" class="anchor"></span>**$code$\]** $text$'
  sections:
    keyword: sec                                          # inline trigger: [sec](#label)
  glossary:
    sources: []                                           # glossary source filenames
  tables:
    keyword: tab                                          # inline trigger: [tab](#label)
    display-prefix: Table                                 # inline ref expands to 'Table N'
    caption-prefix: Table                                 # standalone caption's own lead qualifier
    caption-position: leading                             # caption before the table (gfm-plus/html)
  figures:
    keyword: fig                                          # inline trigger: [fig](#label)
    display-prefix: Figure                                # inline ref expands to 'Figure N'
    caption-prefix: Figure                                # standalone caption's own lead qualifier
    caption-position: trailing                            # caption after the figure (gfm-plus/html)
```

Table anchors use pandoc table-caption syntax: `Table: Caption{#tab:label}`.
Figure anchors use pandoc image attributes: `![alt](path){#fig:label}`.
Both `cite-template` and `entry-template` support `$code$`, `$label$`, and (for
`entry-template`) `$text$` placeholders.

`display-prefix` controls only the inline `[tab](#label)`/`[fig](#label)`
expansion in running prose. In the gfm-plus and HTML channels, nide also
generates the standalone caption itself — pandoc's `gfm` reader (used to build
both) has no `table_captions`/`implicit_figures` support, unlike the `markdown`
reader the PDF/typst channels use. That generated caption is controlled
separately: `caption-prefix` sets its lead qualifier (independent of
`display-prefix`, so prose can read "see table 3" while the caption reads
"Table 3: …"), and `caption-position` (`leading`/`trailing`, any unique
case-insensitive prefix accepted, e.g. `l`) places it before or after the
float. Tables default to leading (a caption ahead of the data often orients
the reader); figures default to trailing (expected to be mostly
self-explanatory). A figure's generated caption text is its image's alt text.
The old `leading: true/false` key is still accepted as a deprecated alias.

### `tables:` - multi-format table input (gfm-plus/html only)

```yaml
tables:
  normalize-gfm-plus: true   # default — accept simple/multiline/grid tables, not just pipe
  on-data-loss: html         # default — html | verbatim (either accepts a unique prefix)
```

Pandoc accepts four table syntaxes (pipe, simple, multiline, grid) in the
`markdown` reader the pdf/typst channels use, but the `gfm` reader (gfm-plus/html)
accepts only pipe tables — same restriction GitHub's own renderer has. With
`normalize-gfm-plus: true` (the default), nide accepts all four in source and
converts non-pipe tables to pipe-table markdown for gfm-plus/html only, via a
pandoc subprocess round-trip — pdf/typst are unaffected, since they already
read all four natively.

Not every table can losslessly become pipe-table markdown — a pipe-table cell
is single-line only, so a grid table with a multi-paragraph or list cell can't
be represented that way. `on-data-loss` controls what happens then: `html`
(default) keeps pandoc's own raw-HTML `<table>` fallback — full rendered
fidelity, at the cost of that one table's source no longer looking like a
table; `verbatim` discards the HTML in favor of wrapping the original block as
a fenced code block — preserves the authored visual layout, at the cost of it
no longer being a real table in the rendered output. Either way a warning is
logged so the loss isn't silent. Pandoc must be on `PATH` for normalization to
run; if it isn't, normalization is skipped with a warning rather than failing
the build.

### `channels:` - channel-specific settings

```yaml
channels:
  html:
    title: 'No Title Given'    # <title> element written by nide inject
    inject:
      css-base: ''             # path to base CSS file (empty = skip)
      css-skin: ''             # path to skin CSS file
      images: false            # inline local images as base64 data URLs
      image-cache: build/image-cache
      intro-trigger: ''        # HTML prefix marking the first authored heading
      logo-data-url: ''        # path to logo data URL .txt file (empty = skip)
      logo-remote-url: ''      # remote logo URL to replace
      toc-trigger: '<h1 id="table-of-contents'
  typst:
    toc-depth: 2               # outline depth (0 = suppress outline)
    toc-dots: false            # dot-leader fill in outline entries
    toc-title: 'Table of Contents'
    code-size: small           # monospace font size: normal | small | footnote
    page-break-l1: false       # weak #pagebreak() before each non-preamble L1 heading
    list-of-figures: false     # emit List of Figures outline after TOC
    list-of-tables: false      # emit List of Tables outline after TOC (LOF first)
    watermark: ''              # diagonal page-background watermark text; '' = none
  pdf:                         # deprecated - use typst channel instead
    logo-local-path: images/OASISLogo-v3.0.png
    logo-remote-url: 'https://docs.oasis-open.org/templates/OASISLogo-v3.0.png'
```

Both `toc-trigger` and `intro-trigger` must be non-empty for TOC replacement to
occur.

### `metadata:` - Dublin Core document metadata

```yaml
metadata:
  title: ''           # dc:title (lang-alt in XMP)
  creator: ''         # dc:creator
  publisher: ''       # dc:publisher (rdf:Alt)
  description: ''     # dc:description
  subject: ''         # dc:subject, comma-separated tags (rdf:Bag)
  date: ''            # publication/creation date in ISO 8601 (e.g. 2026-07-16)
  type: ''            # dc:type (rdf:Bag), e.g. 'Standard'
  identifier: ''      # dc:identifier, persistent URI/DOI
  language: ''        # dc:language (rdf:Bag), BCP 47 tag, e.g. 'en'
  rights: ''          # dc:rights (rdf:Alt), rights statement or license URI
  version: ''         # dcterms:hasVersion, e.g. '1.0'
  status: ''          # dcterms:abstract, e.g. 'DRAFT' or 'Approved'
```

All fields are optional and default to empty string.  Non-empty fields are written into `build/manifest.json` under the `metadata` key after
each `nide manifest` run.
When the HTML channel output (`build/html.md`) is present, non-empty fields are also injected
as `<meta name="DC.*">` / `<meta name="DCTERMS.*">` tags into `<head>` by the manifest embedding pass (DCMI-HTML convention;
`subject` is comma-split into one tag per term).  `nide manifest --inject-pdf PATH` maps non-empty fields to the corresponding XMP namespaces and
writes `<stem>-injected.pdf` alongside the source PDF (requires `pip install nide[manifest]` for pyHanko).

For `channels.typst.code-size`, the valid values and their em sizes are: `normal` (1 em), `small` (0.85 em), `footnote` (0.75 em).
The same names are valid for the `\tablefontsize=` body directive.

### `quality:` - rule-based document quality checks

```yaml
quality:
  rules:
    - etc/rules/spec.rules.yaml   # spec-level rules file (edit this one)
    # - etc/rules/organization.rules.yaml  # org-level base file (fetch, do not edit)
```

`quality.rules` is a list of paths to YAML rules files, resolved relative to the directory
containing `nide.yaml`.  Files are evaluated in the order listed.  Equivalent to
passing `--rules` on the command line.

Each rules file has three top-level keys:

```yaml
meta:
  name: my-spec-rules
  extends: organization          # optional: merge organization.rules.yaml first

context:
  tc-name: dps
  spec-version: "1.0"
  stage: csd02

rules:
  - id: MY-SECTION
    severity: BLOCKER
    description: A required section must be present
    check:
      type: section-present
      pattern: '^Required Section'
      flags: IGNORECASE
```

`section-present` matches `HeadingNode.text` (base heading text only - the section counter lives in `sec_cnt_disp` and must not appear in patterns).
`context` variables are available as `${key}` in pattern and literal values.
`meta.extends: name` merges `<name>.rules.yaml` from the same directory before applying the spec-specific rules.
A spec rule whose `id` matches an inherited org rule replaces it in place - same position,
new severity/pattern/check - instead of adding a duplicate; this lets a spec override one org-level rule without forking the whole org file.

## Options

### Global options

```
nide [-V|--version]
nide version
nide [SUBCOMMAND] --interface FORMAT
```

| Option                | Description                                                     |
|:----------------------|:-----------------------------------------------------------------|
| `-V`, `--version`     | Print version string and exit                                   |
| `version`             | Subcommand alias for `--version`                                 |
| `--interface FORMAT`  | Print the (sub)command's own options/arguments as `text`, `yaml`, or `json`, then exit |

Subcommand names accept any unambiguous prefix: `nide a` = assemble,
`nide r` = render, `nide ej` = eject, `nide ex` = explain, etc.
An unknown or ambiguous subcommand prints an error to stderr and exits with code 2.

`--interface` may follow any subcommand (`nide render --interface yaml`) or stand
alone for the whole tree (`nide --interface json`); both the flag name and the FORMAT
value accept unambiguous prefixes (`--interf=y`). It is recognised before a
subcommand's own required flags are checked, so it works on commands like `render`
that would otherwise demand `-t/--target`. Intended for tooling and shell integration
(it backs `bin/discover.py`'s interface inventory), not everyday interactive use.

### `nide assemble`

```
nide assemble [-d|--debug] [-c PATH|--config PATH] [-t CHANNEL|--target CHANNEL]
```

| Option       | Default         | Description                                                                          |
|:-------------|:----------------|:-------------------------------------------------------------------------------------|
| `-t CHANNEL` | (IR only)       | Compat: build IR + render `CHANNEL` in one step (`gfm-plus`, `html`, `typst`, `pdf`) |
| `-c PATH`    | `etc/nide.yaml` | Path to the assembly configuration file                                              |
| `-d`         | off             | Debug mode: write per-stage snapshot files                                           |

Without `-t`, writes only the channel-neutral IR to `build/ir.json`.

### `nide render`

```
nide render -t CHANNEL[,CHANNEL…] [-c PATH|--config PATH] [-d|--debug]
```

| Option           | Default         | Description                                                 |
|:-----------------|:----------------|:------------------------------------------------------------|
| `-t CHANNEL[,…]` | (required)      | Channels to render: `gfm-plus`, `html`, `typst`, `pdf`; CSV |
| `-c PATH`        | `etc/nide.yaml` | Path to the assembly configuration file                     |
| `-d`             | off             | Enable debug-level logging                                  |

Reads `build/ir.json` (or the path in `output-paths.ir`).  Run `nide assemble` first.

### `nide inject`

```
nide inject [-c PATH|--config PATH]
```

| Option    | Default         | Description                             |
|:----------|:----------------|:----------------------------------------|
| `-c PATH` | `etc/nide.yaml` | Path to the assembly configuration file |

### `nide harvest`

```
nide harvest [-c PATH|--config PATH]
```

| Option    | Default         | Description                             |
|:----------|:----------------|:----------------------------------------|
| `-c PATH` | `etc/nide.yaml` | Path to the assembly configuration file |

### `nide explain`

```
nide explain [-c PATH|--config PATH] [-t CHANNEL|--target CHANNEL]
```

| Option       | Default         | Description                                    |
|:-------------|:----------------|:-----------------------------------------------|
| `-c PATH`    | `etc/nide.yaml` | Path to the assembly configuration file        |
| `-t CHANNEL` | (all channels)  | Limit channel-exclusion display to one channel |

### `nide doctor`

```
nide doctor [-c PATH|--config PATH] [--fix]
```

| Option    | Default         | Description                                       |
|:----------|:----------------|:--------------------------------------------------|
| `-c PATH` | `etc/nide.yaml` | Path to the assembly configuration file           |
| `--fix`   | off             | Create missing directories and run `nide harvest` |

Exit code is 1 if any ERROR-level check fails; 0 otherwise (warnings alone lead to 0).

### `nide eject`

```
nide eject [-o PATH|--output PATH] [-f|--force]
```

| Option    | Default | Description                                    |
|:----------|:--------|:-----------------------------------------------|
| `-o PATH` | stdout  | Write the template to `PATH` instead of stdout |
| `-f`      | off     | Overwrite `PATH` if it already exists          |

### `nide manifest`

```
nide manifest [-c PATH|--config PATH] [--summary [--format text|markdown|yaml]]
nide manifest [-c PATH|--config PATH] --inject-pdf PATH
```

| Option              | Default         | Description                                                                            |
|:--------------------|:----------------|:---------------------------------------------------------------------------------------|
| `-c PATH`           | `etc/nide.yaml` | Path to the assembly configuration file                                                |
| `--summary`         | off             | Read and print existing manifest; do not regenerate                                    |
| `--format FORMAT`   | `text`          | Summary format: `text`, `markdown`, or `yaml`                                          |
| `--inject-pdf PATH` | (none)          | Inject `metadata:` fields as XMP into PDF; writes `<stem>-injected.pdf`; needs pyHanko |

Without `--summary`, `nide manifest` generates (or regenerates) `build/manifest.json`
and embeds blocks in channel outputs.  Requires the `tallipoika` package
(`pip install nide[manifest]`).

With `--summary`, reads the existing manifest and prints to stdout; no tallipoika needed.

`--inject-pdf` writes Dublin Core XMP metadata from the `metadata:` config section
into the PDF.  Exit 1 if pyHanko is absent; exit 2 if the PDF is not found.

### `nide validate`

```
nide validate [-c PATH|--config PATH] [--strict] [--quiet]
```

| Option     | Default         | Description                                              |
|:-----------|:----------------|:---------------------------------------------------------|
| `-c PATH`  | `etc/nide.yaml` | Path to the assembly configuration file                  |
| `--strict` | off             | Treat WARN-level findings as failures (exit 1)           |
| `--quiet`  | off             | Suppress per-check output; only exit code is informative |

Exit code 0: all six checks pass.  Exit code 1: at least one check fails
(or WARN when `--strict`).  Exit code 2: usage error or `build/manifest.json`
not found.

### `nide completion`

```
nide completion bash|fish|zsh
```

| Argument | Description                       |
|:---------|:-----------------------------------|
| `SHELL`  | Target shell: `bash`, `fish`, or `zsh` |

Prints a shell completion script to stdout. Requires the `shtab` package
(`pip install shtab`) — nide prints an install hint and exits 2 if it's missing.

```sh
nide completion bash >> ~/.bash_completion
nide completion zsh  >> ~/.zshrc
nide completion fish > ~/.config/fish/completions/nide.fish
```

Pre-generated scripts are also checked into `docs/completion/nide.{bash,fish,zsh}`
as a reference if you'd rather not install `shtab` yourself.

### Environment

| Variable            | Effect                                                                                                         |
|:--------------------|:---------------------------------------------------------------------------------------------------------------|
| `NIDE_CONFIG`       | Default config path when `-c` is not given (all subcommands)                                                   |
| `NIDE_DEBUG`        | Enable debug output; equivalent to `-d` (assemble and render modes)                                            |
| `DUMP_LUT`          | Write back section LUT files to `etc-path` after assembly                                                      |
| `SOURCE_DATE_EPOCH` | Unix timestamp (seconds since epoch) used by `nide diff --mode repro` to normalise build timestamps; see below |

#### `SOURCE_DATE_EPOCH` and reproducible builds

`SOURCE_DATE_EPOCH` is a [reproducible-builds.org](https://reproducible-builds.org/docs/source-date-epoch/) standard.
When set, `nide diff` in `repro` mode replaces recognised build timestamps - ISO date/datetime values
in `<meta content="…">` attributes and, when `+xmp` is active, in the XMP block - with the canonical UTC
string `YYYY-MM-DDTHH:MM:SSZ` derived from the epoch value, rather than the generic placeholder `[timestamp]`.
This makes two builds of the same source compare as identical regardless of when they were run.

Authors typically anchor the variable to their last VCS commit before building:

```bash
# git
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)

# Fossil
export SOURCE_DATE_EPOCH=$(fossil info tip | awk '/^uuid:/{print $NF}' | xargs fossil time -R . | awk 'NR==1{print $1" "$2}' | date -f - +%s 2>/dev/null)
```

Or set it once in the project Makefile so every `make html pdf` is automatically anchored:

```makefile
SOURCE_DATE_EPOCH ?= $(shell git log -1 --format=%ct)
export SOURCE_DATE_EPOCH
```

Without the variable, `repro` mode still suppresses timestamp noise - timestamps become `[timestamp]`
rather than a real date - so setting it is an opt-in for commit-anchored, bit-reproducible builds.

## Dependencies

Core (always installed):

- [jsonschema](https://pypi.org/project/jsonschema/) - JSON Schema validation
- [kaava](https://pypi.org/project/kaava/) - typed configuration from YAML
- [python-jsonpath](https://pypi.org/project/python-jsonpath/) - JSONPath expression scanning
- [PyYAML](https://pypi.org/project/PyYAML/) - YAML parsing
- [vyyhti](https://pypi.org/project/vyyhti/) - Markdown embedding scanner

Optional - manifest provenance (`pip install nide[manifest]`):

- [tallipoika](https://pypi.org/project/tallipoika/) - RFC 8785 canonical JSON (required for `nide manifest` generation)
- [blake3](https://pypi.org/project/blake3/) - BLAKE3 hashing (enhancement; sha256-only fallback if absent)
- [pyHanko](https://pypi.org/project/pyhanko/) - PDF XMP injection; used by `nide manifest --inject-pdf`

Optional - shell completion (`pip install shtab`):

- [shtab](https://pypi.org/project/shtab/) - generates the bash/fish/zsh scripts `nide completion` prints

## Design and requirements

| Document                            | Identifier  | File                                                      |
|:------------------------------------|:------------|:----------------------------------------------------------|
| Software Requirements Specification | NID-SRS-001 | [docs/requirements/srs/](docs/requirements/srs/README.md) |
| Software Design Description         | NID-SDD-001 | [docs/design/sdd/](docs/design/sdd/README.md)             |

Both documents follow the MIL-STD-498 DID structure and are rendered into
the documentation site alongside the quickstart and tutorial.

## Bug Tracker

Any feature requests or bug reports shall go to the [todos of nide](https://todo.sr.ht/~sthagen/nide).

## Primary Source repository

The main source of `nide` is on a mountain in Central Switzerland under
configuration control ([fossil](https://fossil-scm.org/)).

## Contributions

If you like to share small changes under the repositories license please kindly
do so by sending a patchset.
You can send such a patchset per email using [git send-email](https://git-send-email.io).

## Support

Please kindly submit issues at https://todo.sr.ht/~sthagen/nide or write plain
text email to ~sthagen/nide@lists.sr.ht to support.
Thanks.

## Security Policy

See `SECURITY.md` for the security policy.

## Changes

See [docs/releases/](docs/releases/README.md) for release summaries
and [docs/releases/changes/](docs/releases/changes/README.md) for the detailed change log.

## Coverage

The test suite maintains high branch coverage (≥99%).
The HTML report (if generated) is in `site/coverage/`.

## SBOM

Runtime dependency information is published in `docs/sbom/` in SPDX 3.0 (JSON-LD)
and CycloneDX 1.6 (JSON) formats.
See `docs/sbom/README.md` for the component inventory and validation guide.
